From 0fda88a7acb0e5eb01996fa9fdd7dbb60b598d7c Mon Sep 17 00:00:00 2001 From: Myles Dear Date: Mon, 29 Dec 2025 09:37:40 -0500 Subject: [PATCH 1/9] feat: Add session resilience and context budget management This commit introduces two major features: WebSocket session resilience for surviving temporary disconnects, and proactive context budget management to prevent context window overflow. ## Session Resilience (Backend + Frontend) - JWT-based session security with HttpOnly fingerprint cookie binding (RFC 8725) - Dual heartbeat mechanism (server 30s ping + client 20s heartbeat) - 120-second reconnection grace period with event buffering - Automatic token refresh at 90% of JWT lifespan - Run state preservation during disconnects New files: - core/api/connection_manager.py - Connection state and event buffering - core/api/session_security.py - JWT lifecycle and fingerprint binding - frontend/lib/sessionManager.ts - Client-side session management ## Context Budget Management - Provider-aware token counting (Anthropic, OpenAI, Google APIs) - Circuit breaker pattern: warning at 40%, force completion at 55% - Budget-aware content selection for work module inheritance - Per-worker budget allocation for parallel execution New files: - core/agent_core/llm/token_counter.py - Accurate provider-specific counting - core/agent_core/framework/context_budget_guardian.py - Threshold monitoring - core/agent_core/utils/content_selection.py - Budget-aware inheritance ## Test Coverage - 878 backend unit tests (33 new test files) - 25 frontend tests for session management - Coverage for all new modules ## Documentation - docs/architecture/session-resilience.md - Full design specification - docs/architecture/context-budget-management.md - Budget system design - docs/guides/04-debugging.md - Added CLI tools documentation - scripts/analyze_session.py - Session analysis utility - scripts/commonground.sh - Service manager script ## Other Changes - Graceful shutdown with connection cleanup - Updated pyproject.toml with uv export instructions - Anthropic-specific LLM configs for accurate token budgeting - Agent profile updates for budget-aware operation --- .gitignore | 9 +- core/.gitignore | 4 +- core/agent_core/config/app_config.py | 172 +- core/agent_core/events/ingestors.py | 196 +- .../framework/context_budget_guardian.py | 602 + core/agent_core/framework/tool_registry.py | 246 +- core/agent_core/iic/core/event_handler.py | 44 +- core/agent_core/llm/call_llm.py | 109 +- core/agent_core/llm/config_resolver.py | 41 +- core/agent_core/llm/token_counter.py | 453 + core/agent_core/nodes/base_agent_node.py | 406 +- .../nodes/custom_nodes/dispatcher_node.py | 277 +- .../nodes/custom_nodes/finish_node.py | 240 +- .../custom_nodes/get_principal_status_tool.py | 184 +- core/agent_core/nodes/mcp_proxy_node.py | 131 +- core/agent_core/services/server_manager.py | 147 +- core/agent_core/state/management.py | 92 +- core/agent_core/utils/content_selection.py | 464 + .../principal_to_associate_briefing.yaml | 38 +- .../llm_configs/associate_llm.yaml | 16 +- .../llm_configs/embedding_model.yaml | 18 +- .../llm_configs/fast_utils_llm.yaml | 12 +- .../llm_configs/principal_llm.yaml | 33 +- .../profiles/Associate_Analyst_Academic.yaml | 2 +- .../Associate_GenericExecutor_EN.yaml | 5 +- .../profiles/Associate_LocalRAG_EN.yaml | 34 +- .../profiles/Associate_SmartRAG_EN.yaml | 21 +- .../Associate_WebSearcher_Academic.yaml | 10 +- .../Associate_WebSearcher_Business.yaml | 8 +- .../profiles/Associate_WebSearcher_EN.yaml | 31 +- .../Associate_WebSearcher_Technical.yaml | 14 +- core/agent_profiles/profiles/Base_Agent.yaml | 8 +- .../profiles/Base_Associate_EN.yaml | 1 + .../profiles/Partner_StrategicAdvisor_EN.yaml | 41 +- .../profiles/Principal_Planner_EN.yaml | 39 +- core/api/connection_manager.py | 755 + core/api/events.py | 157 +- core/api/message_handlers.py | 358 +- core/api/server.py | 347 +- core/api/session.py | 199 +- core/api/session_security.py | 476 + core/env.sample | 36 + core/mcp.json | 26 +- core/pyproject.toml | 38 + core/rag_configs/internal_project_docs.yaml | 14 +- core/requirements-dev.txt | 585 + core/requirements.txt | Bin 6302 -> 11344 bytes core/tests/__init__.py | 1 + core/tests/conftest.py | 84 + core/tests/test_app_config.py | 472 + core/tests/test_base_tool_node.py | 546 + core/tests/test_call_llm.py | 814 + core/tests/test_clean_messages_for_llm.py | 548 + core/tests/test_config_resolver.py | 403 + core/tests/test_connection_manager.py | 597 + core/tests/test_content_selection.py | 604 + core/tests/test_context_budget_guardian.py | 368 + core/tests/test_context_helpers.py | 419 + core/tests/test_dynamic_loader.py | 149 + core/tests/test_embedding_utils.py | 573 + core/tests/test_event_strategies.py | 698 + core/tests/test_finish_node.py | 346 + core/tests/test_handover_service.py | 562 + core/tests/test_inbox_processor.py | 571 + core/tests/test_ingestors.py | 256 + core/tests/test_jina_api.py | 229 + core/tests/test_llm_utils.py | 364 + core/tests/test_mcp_reconnection.py | 289 + core/tests/test_message_utils.py | 393 + core/tests/test_models.py | 496 + core/tests/test_profile_loading.py | 212 + core/tests/test_profile_utils.py | 482 + core/tests/test_search_engine.py | 749 + .../tests/test_session_resilience_handlers.py | 303 + core/tests/test_session_security.py | 526 + core/tests/test_stale_session_detection.py | 243 + core/tests/test_token_counter.py | 313 + core/tests/test_tool_registry.py | 615 + core/tests/test_turn_manager.py | 538 + core/uv.lock | 2758 ++-- docs/architecture.md | 11 + .../architecture/context-budget-management.md | 528 + docs/architecture/session-resilience.md | 489 + docs/framework/02-team-collaboration.md | 38 +- docs/guides/01-agent-profiles.md | 101 +- docs/guides/03-advanced-customization.md | 40 +- docs/guides/04-debugging.md | 103 + docs/guides/06-built-in-tools.md | 76 +- frontend/README_TEST.md | 181 + frontend/__mocks__/fileMock.js | 1 + frontend/__mocks__/styleMock.js | 1 + frontend/__tests__/sessionManager.test.ts | 496 + frontend/app/r/page.tsx | 20 +- frontend/app/stores/sessionStore.ts | 366 +- frontend/components/layout/AppSidebar.tsx | 138 +- frontend/jest.config.js | 61 + frontend/jest.setup.ts | 40 + frontend/lib/api.ts | 98 +- frontend/lib/sessionManager.ts | 595 + frontend/package-lock.json | 13727 ++++++++++------ frontend/package.json | 12 +- frontend/yarn.lock | 2665 ++- scripts/analyze_session.py | 922 ++ scripts/commonground.sh | 417 + 104 files changed, 37488 insertions(+), 7298 deletions(-) create mode 100644 core/agent_core/framework/context_budget_guardian.py create mode 100644 core/agent_core/llm/token_counter.py create mode 100644 core/agent_core/utils/content_selection.py create mode 100644 core/api/connection_manager.py create mode 100644 core/api/session_security.py create mode 100644 core/requirements-dev.txt create mode 100644 core/tests/__init__.py create mode 100644 core/tests/conftest.py create mode 100644 core/tests/test_app_config.py create mode 100644 core/tests/test_base_tool_node.py create mode 100644 core/tests/test_call_llm.py create mode 100644 core/tests/test_clean_messages_for_llm.py create mode 100644 core/tests/test_config_resolver.py create mode 100644 core/tests/test_connection_manager.py create mode 100644 core/tests/test_content_selection.py create mode 100644 core/tests/test_context_budget_guardian.py create mode 100644 core/tests/test_context_helpers.py create mode 100644 core/tests/test_dynamic_loader.py create mode 100644 core/tests/test_embedding_utils.py create mode 100644 core/tests/test_event_strategies.py create mode 100644 core/tests/test_finish_node.py create mode 100644 core/tests/test_handover_service.py create mode 100644 core/tests/test_inbox_processor.py create mode 100644 core/tests/test_ingestors.py create mode 100644 core/tests/test_jina_api.py create mode 100644 core/tests/test_llm_utils.py create mode 100644 core/tests/test_mcp_reconnection.py create mode 100644 core/tests/test_message_utils.py create mode 100644 core/tests/test_models.py create mode 100644 core/tests/test_profile_loading.py create mode 100644 core/tests/test_profile_utils.py create mode 100644 core/tests/test_search_engine.py create mode 100644 core/tests/test_session_resilience_handlers.py create mode 100644 core/tests/test_session_security.py create mode 100644 core/tests/test_stale_session_detection.py create mode 100644 core/tests/test_token_counter.py create mode 100644 core/tests/test_tool_registry.py create mode 100644 core/tests/test_turn_manager.py create mode 100644 docs/architecture/context-budget-management.md create mode 100644 docs/architecture/session-resilience.md create mode 100644 frontend/README_TEST.md create mode 100644 frontend/__mocks__/fileMock.js create mode 100644 frontend/__mocks__/styleMock.js create mode 100644 frontend/__tests__/sessionManager.test.ts create mode 100644 frontend/jest.config.js create mode 100644 frontend/jest.setup.ts create mode 100644 frontend/lib/sessionManager.ts create mode 100755 scripts/analyze_session.py create mode 100755 scripts/commonground.sh diff --git a/.gitignore b/.gitignore index fbe36ff..12d2bb5 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,11 @@ __pycache__/ frontend/env.local frontend/node_modules/ -repomix-* \ No newline at end of file +repomix-* + +# PID files for frontend and backend +*.pid + +# Local logs +logs +logs/* diff --git a/core/.gitignore b/core/.gitignore index b5dd7a8..0dd6882 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -39,7 +39,7 @@ out/ Thumbs.db # Log files -logs/ +logs/* *.log # Temporary & Cache files @@ -100,4 +100,4 @@ bug_commit.txt !/runs/.gitignore !/.github -.claude \ No newline at end of file +.claude diff --git a/core/agent_core/config/app_config.py b/core/agent_core/config/app_config.py index 83350e3..428f699 100644 --- a/core/agent_core/config/app_config.py +++ b/core/agent_core/config/app_config.py @@ -1,18 +1,38 @@ import os import json import logging +import threading +import copy +from types import MappingProxyType +from typing import Dict, Any logger = logging.getLogger(__name__) -def load_native_mcp_config(): +# Thread-safe MCP configuration loader +# Uses a lock to ensure atomic loading and immutable proxy for read access +_mcp_config_lock = threading.Lock() +_mcp_server_categories_internal: Dict[str, Any] = {} +_native_mcp_servers_internal: Dict[str, Any] = {} +_mcp_config_loaded = False + + +def _load_mcp_config_internal() -> tuple[Dict[str, Any], Dict[str, Any]]: """ - Loads native MCP server configurations from environment variables or a configuration file. - - Returns: - dict: A dictionary of enabled MCP server configurations. + Internal function to load MCP configuration. + Returns tuple of (enabled_servers, categories). + + IMPORTANT: Category-based toolset matching is STRICT: + - Servers WITHOUT an explicit 'category' field default to 'uncategorized' + - 'uncategorized' servers are NOT matched by 'all_user_specified_mcp_servers' or 'all_google_related_mcp_servers' + - To include a server in a category toolset, you MUST explicitly set its 'category' field + + Standard categories: + - "google_related": Google/Gemini ecosystem servers + - "user_specified": User-added domain-specific servers + - "uncategorized": Default for servers without explicit category (NOT matched by category toolsets) """ config_str = os.getenv("NATIVE_MCP_SERVERS_CONFIG") - config_path = os.getenv("NATIVE_MCP_SERVERS_CONFIG_PATH", "mcp.json") # Add default value + config_path = os.getenv("NATIVE_MCP_SERVERS_CONFIG_PATH", "mcp.json") config = {} if config_path and os.path.exists(config_path): @@ -29,21 +49,135 @@ def load_native_mcp_config(): except json.JSONDecodeError as e: logger.error("native_mcp_config_env_parse_failed", extra={"error": str(e)}) - # Return only enabled server configurations + # Build category mapping and return only enabled server configurations enabled_servers = {} + categories = {} + if isinstance(config, dict) and "mcpServers" in config: - for name, server_conf in config["mcpServers"].items(): - if isinstance(server_conf, dict) and server_conf.get("enabled", False): - # [MODIFIED] Only require the transport field to be present - if "transport" in server_conf: - enabled_servers[name] = server_conf - else: + for name, server_conf in config["mcpServers"].items(): + if not isinstance(server_conf, dict): + continue + + # STRICT category handling: default to 'uncategorized' if not specified + # 'uncategorized' servers will NOT be matched by category-based toolsets + explicit_category = server_conf.get("category") + category = explicit_category if explicit_category else "uncategorized" + + if not explicit_category: + logger.warning("mcp_server_missing_category", extra={ + "server_name": name, + "hint": "Server has no 'category' field. It will NOT be included in category-based toolsets like 'all_user_specified_mcp_servers'. Add 'category': 'user_specified' to include it." + }) + + categories[name] = { + "category": category, + "enabled": server_conf.get("enabled", False), + "has_explicit_category": explicit_category is not None + } + + if server_conf.get("enabled", False): + if "transport" in server_conf: + enabled_servers[name] = server_conf + else: logger.warning("native_mcp_server_config_incomplete", extra={"server_name": name, "missing_field": "transport"}) - else: - logger.debug("native_mcp_server_disabled_or_invalid", extra={"server_name": name}) + else: + logger.debug("native_mcp_server_disabled_or_invalid", extra={"server_name": name}) + + logger.info("native_mcp_servers_loaded", extra={"enabled_count": len(enabled_servers), "categories": categories}) + return enabled_servers, categories + + +def _ensure_mcp_config_loaded() -> None: + """ + Thread-safe initialization of MCP configuration. + Uses double-checked locking pattern for efficiency. + """ + global _mcp_config_loaded, _mcp_server_categories_internal, _native_mcp_servers_internal + + if _mcp_config_loaded: + return + + with _mcp_config_lock: + # Double-check after acquiring lock + if _mcp_config_loaded: + return + + servers, categories = _load_mcp_config_internal() + _native_mcp_servers_internal = servers + _mcp_server_categories_internal = categories + _mcp_config_loaded = True + + +def get_mcp_server_categories() -> MappingProxyType: + """ + Returns an immutable view of MCP server categories. + Thread-safe and prevents accidental modification. + """ + _ensure_mcp_config_loaded() + return MappingProxyType(_mcp_server_categories_internal) + + +def get_native_mcp_servers() -> Dict[str, Any]: + """ + Returns the enabled MCP server configurations. + Thread-safe initialization on first access. + + Returns a deep copy to prevent callers from mutating the internal state, + since MCP server configs contain nested dicts (transport params, etc.). + """ + _ensure_mcp_config_loaded() + return copy.deepcopy(_native_mcp_servers_internal) + + +def reload_mcp_config() -> None: + """ + Force reload of MCP configuration. + Thread-safe - acquires lock before reloading. + Use sparingly, typically only for hot-reload scenarios. + """ + global _mcp_config_loaded, _mcp_server_categories_internal, _native_mcp_servers_internal + + with _mcp_config_lock: + servers, categories = _load_mcp_config_internal() + _native_mcp_servers_internal = servers + _mcp_server_categories_internal = categories + _mcp_config_loaded = True + logger.info("mcp_config_reloaded") + + +# Backward compatibility: Module-level access via property-like pattern +# These trigger lazy initialization on first access +class _MCPConfigProxy: + """Proxy class to provide backward-compatible module-level attribute access.""" + + @property + def MCP_SERVER_CATEGORIES(self) -> MappingProxyType: + return get_mcp_server_categories() + + @property + def NATIVE_MCP_SERVERS(self) -> Dict[str, Any]: + return get_native_mcp_servers() + + +# For backward compatibility, expose as module-level variables +# Note: These are now lazy-loaded and thread-safe +def _get_mcp_server_categories(): + """Backward-compatible accessor for MCP_SERVER_CATEGORIES.""" + return get_mcp_server_categories() + + +def _get_native_mcp_servers(): + """Backward-compatible accessor for NATIVE_MCP_SERVERS.""" + return get_native_mcp_servers() + - logger.info("native_mcp_servers_loaded", extra={"enabled_count": len(enabled_servers)}) - return enabled_servers +# Trigger initial load for backward compatibility +# This ensures the config is loaded at module import time as before +_ensure_mcp_config_loaded() -# Global configuration -NATIVE_MCP_SERVERS = load_native_mcp_config() +# Expose read-only views for backward compatibility +# WARNING: These are snapshots at import time. Use get_*() functions for guaranteed fresh access. +# MCP_SERVER_CATEGORIES uses MappingProxyType to prevent mutation +# NATIVE_MCP_SERVERS returns a deepcopy to prevent mutation of nested dicts +MCP_SERVER_CATEGORIES = MappingProxyType(_mcp_server_categories_internal) +NATIVE_MCP_SERVERS = copy.deepcopy(_native_mcp_servers_internal) diff --git a/core/agent_core/events/ingestors.py b/core/agent_core/events/ingestors.py index 8828e68..323b4de 100644 --- a/core/agent_core/events/ingestors.py +++ b/core/agent_core/events/ingestors.py @@ -47,19 +47,19 @@ def templated_content_ingestor(payload: Any, params: Dict, context: Dict) -> str content_key = payload["content_key"] loaded_profile = context.get("loaded_profile", {}) text_definitions = loaded_profile.get("text_definitions", {}) - + template_string = text_definitions.get(content_key) - + if not template_string: logger.error("content_key_not_found_in_profile", extra={"content_key": content_key, "profile_name": loaded_profile.get('name')}) return f"[Error: Template '{content_key}' not found]" rendered_content = _apply_simple_template_interpolation(template_string, context) - + wrapper_tags = params.get("wrapper_tags") if wrapper_tags and isinstance(wrapper_tags, list) and len(wrapper_tags) == 2: return f"{wrapper_tags[0]}{rendered_content}{wrapper_tags[1]}" - + return rendered_content @register_ingestor("generic_message_ingestor") @@ -85,7 +85,7 @@ def tool_result_ingestor(payload: Any, params: Dict, context: Dict) -> str: # If it's a dehydrated token, return it directly if isinstance(content, str) and content.startswith("<#CGKB-"): return content - + # For errors, use JSON to preserve full context with clear wrapping tags if is_error: error_report = { @@ -116,11 +116,11 @@ def markdown_formatter_ingestor(payload: Any, params: Dict, context: Dict) -> st """Converts a payload (usually a dictionary) into a Markdown list.""" if not isinstance(payload, dict): return str(payload) - + lines = [] title = params.get("title", "### Contextual Information") lines.append(title) - + key_renames = params.get("key_renames", {}) exclude_keys = params.get("exclude_keys", []) @@ -129,23 +129,72 @@ def markdown_formatter_ingestor(payload: Any, params: Dict, context: Dict) -> st continue display_key = key_renames.get(key, key.replace('_', ' ').title()) lines.append(f"* **{display_key}**: {value}") - + return "\n".join(lines) @register_ingestor("work_modules_ingestor") def work_modules_ingestor(payload: Any, params: Dict, context: Dict) -> str: - """Formats work_modules dictionary as Markdown using the generic formatter.""" + """ + Formats work_modules dictionary as Markdown for context injection. + + CRITICAL: This ingestor filters out large fields like context_archive to prevent + context window explosion. Full archives are stored in team_state but never injected. + Use dispatch_result_ingestor for deliverables from completed work. + """ if not isinstance(payload, dict): return "Work modules data is not in the expected format (dictionary)." - + + # Fields to EXCLUDE from injection - these can be 100K+ chars each + EXCLUDED_FIELDS = { + 'context_archive', # Full message history - stored but never injected + 'full_context', # Any full context dumps + 'raw_messages', # Raw message lists + 'messages', # Message arrays + 'new_messages_from_associate', # Associate work history + } + + # Fields to SUMMARIZE (show counts/metadata only) + SUMMARIZE_FIELDS = { + 'deliverables': lambda v: f"({len(v)} items)" if isinstance(v, dict) else "(present)" if v else "(none)", + 'tools_used': lambda v: ', '.join(v[:5]) + ('...' if len(v) > 5 else '') if isinstance(v, list) else str(v), + } + + def _filter_module(module_data: dict) -> dict: + """Filter a single work module to exclude large fields, preventing context explosion.""" + filtered = {} + for key, value in module_data.items(): + if key in EXCLUDED_FIELDS: + # Log when we skip large fields for debugging + if isinstance(value, (list, dict)) and len(str(value)) > 1000: + logger.debug("work_modules_ingestor_field_excluded", extra={ + "field": key, + "approx_size": len(str(value)) + }) + continue + if key in SUMMARIZE_FIELDS: + filtered[key] = SUMMARIZE_FIELDS[key](value) + elif isinstance(value, dict) and len(str(value)) > 5000: + # Recursively filter nested dicts that are too large + filtered[key] = _filter_module(value) + else: + filtered[key] = value + return filtered + lines = [params.get("title", "### Current Work Modules Status")] if not payload: lines.append("No work modules are currently defined.") else: - # Use the formatter to handle the entire dictionary - formatted_modules = _recursive_markdown_formatter(payload, {}, level=0) + # Filter each module before formatting to prevent context explosion + filtered_modules = {} + for mod_id, mod_data in payload.items(): + if isinstance(mod_data, dict): + filtered_modules[mod_id] = _filter_module(mod_data) + else: + filtered_modules[mod_id] = mod_data + + formatted_modules = _recursive_markdown_formatter(filtered_modules, {}, level=0) lines.extend(formatted_modules) - + return "\n".join(lines) @register_ingestor("available_associates_ingestor") @@ -153,6 +202,7 @@ def available_associates_ingestor(payload: Any, params: Dict, context: Dict) -> """Formats available Associate list as Markdown with separated data preparation and presentation logic.""" profile_instance_ids = payload if not isinstance(profile_instance_ids, list): + logger.warning("available_associates_ingestor_invalid_payload", extra={"payload_type": type(payload).__name__}) return "Available associates list is not in the expected format (list)." agent_profiles_store = context.get('refs', {}).get('run', {}).get('config', {}).get('agent_profiles_store') @@ -160,13 +210,28 @@ def available_associates_ingestor(payload: Any, params: Dict, context: Dict) -> logger.error("available_associates_ingestor: 'agent_profiles_store' not found.") return "Error: Profile store not available." + logger.info("available_associates_ingestor_processing", extra={ + "payload_count": len(profile_instance_ids), + "store_count": len(agent_profiles_store) + }) + # Step 1: Prepare a clean, structured list of Python objects profiles_for_llm = [] for instance_id in profile_instance_ids: profile_dict = get_profile_by_instance_id(agent_profiles_store, instance_id) - if not profile_dict or profile_dict.get("is_deleted") or not profile_dict.get("is_active") or profile_dict.get("type") != "associate": + if not profile_dict: + logger.info("available_associates_ingestor_profile_not_found", extra={"instance_id": instance_id}) + continue + if profile_dict.get("is_deleted"): + logger.info("available_associates_ingestor_profile_deleted", extra={"instance_id": instance_id}) + continue + if not profile_dict.get("is_active"): + logger.info("available_associates_ingestor_profile_inactive", extra={"instance_id": instance_id}) + continue + if profile_dict.get("type") != "associate": + logger.info("available_associates_ingestor_wrong_type", extra={"instance_id": instance_id, "type": profile_dict.get("type")}) continue - + profiles_for_llm.append({ "profile_name": profile_dict.get('name', 'Unknown'), "description": profile_dict.get('description_for_human', 'No description.'), @@ -181,7 +246,7 @@ def available_associates_ingestor(payload: Any, params: Dict, context: Dict) -> sorted_profiles = sorted(profiles_for_llm, key=lambda p: p.get('profile_name', '')) formatted_profiles = _recursive_markdown_formatter(sorted_profiles, {}, level=0) lines.extend(formatted_profiles) - + return "\n".join(lines) @register_ingestor("principal_history_summary_ingestor") @@ -200,9 +265,9 @@ def principal_history_summary_ingestor(payload: Any, params: Dict, context: Dict role = msg.get("role", "unknown_role") content_summary = str(msg.get("content", "")) tool_calls = msg.get("tool_calls") - + entry = f"\n- **[{role.upper()}]**: {content_summary[:200]}{'...' if len(content_summary) > 200 else ''}" - + if tool_calls and isinstance(tool_calls, list): tools_called_parts = [] for tc in tool_calls: @@ -212,12 +277,12 @@ def principal_history_summary_ingestor(payload: Any, params: Dict, context: Dict tools_called_parts.append(f"{func_name}({args_summary})") if tools_called_parts: entry += f" -> Calls: [{', '.join(tools_called_parts)}]" - + output_parts.append(entry) - + if len(payload) > max_messages: output_parts.append(f"\n... (omitting {len(payload) - max_messages} older messages)") - + output_parts.append("\n") return "\n".join(output_parts) @@ -227,7 +292,7 @@ def json_history_ingestor(payload: Any, params: Dict, context: Dict) -> str: if not isinstance(payload, list): logger.warning("json_history_ingestor_expected_list", extra={"payload_type": type(payload).__name__}) return "[Error: Message history for JSON ingestion was not a list.]" - + try: history_json_string = json.dumps(payload, ensure_ascii=False, indent=2) return f"\n{history_json_string}\n" @@ -240,10 +305,10 @@ def tagged_content_ingestor(payload: Any, params: Dict, context: Dict) -> str: """Wraps the payload content with specified XML tags.""" wrapper_tags = params.get("wrapper_tags") content = str(payload) - + if wrapper_tags and isinstance(wrapper_tags, list) and len(wrapper_tags) == 2: return f"{wrapper_tags[0]}{content}{wrapper_tags[1]}" - + logger.warning("tagged_content_ingestor_missing_wrapper_tags") return content @@ -274,12 +339,21 @@ def observer_failure_ingestor(payload: Any, params: Dict, context: Dict) -> str: @register_ingestor("dispatch_result_ingestor") def dispatch_result_ingestor(payload: Any, params: Dict, context: Dict) -> str: - """Formats 'dispatch_submodules' results into detailed, human and LLM-readable Markdown reports.""" + """ + Formats 'dispatch_submodules' results into Markdown reports. + + DESIGN PRINCIPLE: No truncation here. The Associate is responsible for + intelligent summarization within their budget. We present their deliverables + in full since they've already been summarized appropriately. + + Full message archives are stored in work_modules.context_archive for detailed + review if the Principal needs to drill down. + """ if not isinstance(payload, dict) or "content" not in payload: return "[Error: Dispatch result format is invalid or content is missing]" - + content = payload.get("content", {}) - + # Overall operation summary overall_status = content.get('status', 'UNKNOWN') message = content.get('message', 'No message.') @@ -288,46 +362,56 @@ def dispatch_result_ingestor(payload: Any, params: Dict, context: Dict) -> str: f"- **Overall Status**: `{overall_status}`", f"- **Details**: {message}" ] - + # Failed preparation tasks failed_prep = content.get("failed_preparation_details", []) if failed_prep: summary_parts.append("\n**Assignments Failed Before Execution:**") - # Use the formatter to show failure details summary_parts.extend(_recursive_markdown_formatter(failed_prep, {}, level=0)) - # Detailed work records of executed modules + # Work records of executed modules - deliverables in full (already summarized by associate) exec_results = content.get("assignment_execution_results", []) if exec_results: - summary_parts.append("\n**Executed Modules - Detailed Work Records:**") + summary_parts.append("\n**Executed Modules - Results:**") for result in exec_results: module_id = result.get('module_id', 'N/A') exec_status = result.get('execution_status', 'unknown') - - summary_parts.append(f"\n--- Start of Record for Module `{module_id}` (Status: `{exec_status}`) ---") - - # Display final deliverables - deliverables = result.get('deliverables', {}) - summary_parts.append("#### Final Deliverable (from Associate):") - summary_parts.extend(_recursive_markdown_formatter(deliverables, {}, level=0)) + associate_id = result.get('associate_id', 'N/A') - # Display the full net-added message history + summary_parts.append(f"\n#### Module `{module_id}` (Status: `{exec_status}`, Agent: `{associate_id}`)") + + # Display final deliverables IN FULL - associate has already done smart summarization + deliverables = result.get('deliverables', {}) + if deliverables: + summary_parts.append("**Final Deliverable:**") + summary_parts.extend(_recursive_markdown_formatter(deliverables, {}, level=0)) + else: + summary_parts.append("*No deliverables provided.*") + + # Display error details if any + error_details = result.get('error_details') + if error_details: + summary_parts.append(f"**Error:** {error_details}") + + # Provide work metadata (not full history - that's in context_archive) new_messages = result.get('new_messages_from_associate', []) if new_messages: - summary_parts.append("\n#### Full Work Log from Associate:") + tools_used = [] for msg in new_messages: - role = msg.get("role", "unknown").upper() - msg_content = str(msg.get("content", "[No Content]")).strip() tool_calls = msg.get("tool_calls") - if tool_calls: - summary_parts.append(f"**[{role} -> TOOL_CALL]**:") - summary_parts.extend(_recursive_markdown_formatter(tool_calls, {}, level=1)) - elif msg_content: - summary_parts.append(f"**[{role}]**: {msg_content}") - - summary_parts.append(f"--- End of Record for Module `{module_id}` ---\n") - + for tc in tool_calls: + tool_name = tc.get("function", {}).get("name", "unknown") + if tool_name not in tools_used: + tools_used.append(tool_name) + + if tools_used: + summary_parts.append(f"**Tools Used:** {', '.join(tools_used)}") + summary_parts.append(f"**Work Steps:** {len(new_messages)} messages exchanged") + summary_parts.append("*(Full work log available in module's context_archive if needed)*") + + summary_parts.append("") # Empty line between modules + return "\n".join(summary_parts) @register_ingestor("user_prompt_ingestor") @@ -355,7 +439,7 @@ def _recursive_markdown_formatter(data: Any, schema: Dict, level: int = 0) -> Li sub_lines = _recursive_markdown_formatter(value, prop_schema, level + 1) lines.extend(sub_lines) return lines - + # Smart fallback logic when no detailed schema is available if isinstance(data, dict): if not data: @@ -381,7 +465,7 @@ def _recursive_markdown_formatter(data: Any, schema: Dict, level: int = 0) -> Li else: # Handle other primitive types lines.append(f"{indent} {str(data)}") - + return lines @register_ingestor("protocol_aware_ingestor") @@ -393,16 +477,16 @@ def protocol_aware_ingestor(payload: Any, params: Dict, context: Dict) -> str: data = payload["data"] schema = payload["schema_for_rendering"] - + # Use top-level title from schema if available top_level_title = schema.get("x-handover-title", "Agent Briefing") - + lines = [f"## {top_level_title}"] - + # Start the recursive formatting formatted_lines = _recursive_markdown_formatter(data, schema, level=0) lines.extend(formatted_lines) - + return "\n".join(lines) logger.info("ingestor_registry_initialized", extra={"count": len(INGESTOR_REGISTRY), "ingestors": list(INGESTOR_REGISTRY.keys())}) diff --git a/core/agent_core/framework/context_budget_guardian.py b/core/agent_core/framework/context_budget_guardian.py new file mode 100644 index 0000000..fda56ae --- /dev/null +++ b/core/agent_core/framework/context_budget_guardian.py @@ -0,0 +1,602 @@ +""" +Context Budget Guardian - Circuit breaker for agent context window management. + +This module provides proactive context window monitoring to prevent agents from +hitting ContextWindowExceededError by forcing graceful completion when approaching +the limit. + +Design Principles: +- Monitor, don't truncate: We track consumption and trigger early completion +- Respect agent autonomy: At warning threshold, inject guidance; at critical, force action +- Fail gracefully: Even at critical threshold, we guide to completion rather than crash +""" + +import logging +from typing import Dict, Optional, Tuple +from enum import Enum, auto + +logger = logging.getLogger(__name__) + + +class ContextBudgetStatus(Enum): + """Status levels for context budget consumption.""" + HEALTHY = auto() # < 40% - Normal operation + WARNING = auto() # 40-55% - Inject guidance to wrap up + CRITICAL = auto() # 55-70% - Force immediate completion + EXCEEDED = auto() # > 70% - Circuit breaker, 30% remains for wrap-up + + +# Default context limits by model family (tokens) +# These are conservative estimates to leave room for response +DEFAULT_CONTEXT_LIMITS = { + "anthropic/claude-sonnet-4": 200000, + "anthropic/claude-sonnet-4-5": 200000, + "anthropic/claude-3-5-haiku": 200000, + "anthropic/claude-3-5-sonnet": 200000, + "anthropic/claude-3-opus": 200000, + "openai/gpt-4o": 128000, + "openai/gpt-4-turbo": 128000, + "openai/gpt-4": 8192, + "openai/gpt-3.5-turbo": 16385, + "gemini/gemini-2.5-pro": 1000000, + "gemini/gemini-2.5-flash": 1000000, +} + +# Models that support extended 1M context with the anthropic-beta header +MODELS_SUPPORTING_1M_CONTEXT = { + "anthropic/claude-sonnet-4-5", + "anthropic/claude-3-5-sonnet", +} + +# Threshold percentages +# These are set to leave 30% headroom for final summarization/wrap-up +WARNING_THRESHOLD = 0.40 # 40% - Start suggesting wrap-up +CRITICAL_THRESHOLD = 0.55 # 55% - Force completion +EXCEEDED_THRESHOLD = 0.70 # 70% - Circuit breaker triggers, 30% remains for wrap-up + +# Budget allocation constants +SUMMARIZATION_RESERVE_PERCENT = 0.30 # Reserve 30% for summarization and wrap-up + + +def calculate_worker_budget( + model_context_limit: int, + num_workers: int, + base_context_overhead: int = 30000, + summarization_reserve: float = SUMMARIZATION_RESERVE_PERCENT +) -> Dict: + """ + Calculate the per-worker token budget based on model capacity and number of workers. + + Algorithm: + 1. Start with model's max context limit + 2. Subtract base context overhead (system prompt, tools, etc.) + 3. Reserve 30% for summarization and wrap-up + 4. Divide remaining 70% evenly among workers + + Args: + model_context_limit: The model's maximum context window (e.g., 200000 or 1000000) + num_workers: Number of parallel worker agents + base_context_overhead: Fixed overhead for system prompt, tools, etc. (default 30K) + summarization_reserve: Fraction to reserve for summarization (default 0.30) + + Returns: + Dict with budget allocation details: + - total_available: Total context after overhead + - summarization_budget: Reserved for wrap-up + - worker_budget_total: Total budget for all workers + - per_worker_budget: Budget per individual worker + - num_workers: Number of workers + """ + # Available context after base overhead + total_available = model_context_limit - base_context_overhead + + # Reserve portion for summarization + summarization_budget = int(total_available * summarization_reserve) + + # Remaining budget for workers + worker_budget_total = total_available - summarization_budget + + # Per-worker allocation + per_worker_budget = worker_budget_total // num_workers if num_workers > 0 else worker_budget_total + + result = { + "model_context_limit": model_context_limit, + "base_context_overhead": base_context_overhead, + "total_available": total_available, + "summarization_budget": summarization_budget, + "summarization_reserve_percent": summarization_reserve * 100, + "worker_budget_total": worker_budget_total, + "per_worker_budget": per_worker_budget, + "num_workers": num_workers + } + + logger.info("worker_budget_calculated", extra=result) + return result + + +def get_model_context_limit(model_name: str, llm_config: Optional[Dict] = None) -> int: + """ + Gets the context limit for a given model. + + Priority: + 1. Explicit max_context_tokens in llm_config + 2. 1M context if model supports it AND extra_headers contains anthropic-beta context header + 3. Model family match in DEFAULT_CONTEXT_LIMITS + 4. Conservative default (100000) + + Args: + model_name: The model identifier (e.g., "anthropic/claude-sonnet-4-20250514") + llm_config: Optional LLM configuration dict that may contain max_context_tokens + + Returns: + The context token limit for the model + """ + # Check explicit config first + if llm_config and llm_config.get("max_context_tokens"): + return llm_config["max_context_tokens"] + + # Check if 1M context is enabled via extra_headers + if llm_config: + extra_headers = llm_config.get("extra_headers") + if extra_headers and isinstance(extra_headers, dict): + beta_header = extra_headers.get("anthropic-beta", "") + if "context-1m" in beta_header or "1m" in beta_header.lower(): + # Check if model supports 1M context + for model_prefix in MODELS_SUPPORTING_1M_CONTEXT: + if model_name.startswith(model_prefix): + logger.info("context_limit_1m_enabled", extra={ + "model_name": model_name, + "limit": 1000000 + }) + return 1000000 + + # Try exact match + if model_name in DEFAULT_CONTEXT_LIMITS: + return DEFAULT_CONTEXT_LIMITS[model_name] + + # Try prefix match (handles versioned model names like claude-sonnet-4-20250514) + for model_prefix, limit in DEFAULT_CONTEXT_LIMITS.items(): + if model_name.startswith(model_prefix): + return limit + + # Try matching without provider prefix (e.g., "claude-sonnet-4-20250514" should match "anthropic/claude-sonnet-4") + for model_prefix, limit in DEFAULT_CONTEXT_LIMITS.items(): + # Extract the model family part after the provider prefix (e.g., "claude-sonnet-4" from "anthropic/claude-sonnet-4") + if "/" in model_prefix: + model_family = model_prefix.split("/", 1)[1] + if model_name.startswith(model_family): + return limit + + # Conservative default + logger.warning("context_limit_unknown_model", extra={ + "model_name": model_name, + "default_limit": 100000, + "hint": "Add max_context_tokens to LLM config for accurate limit" + }) + return 100000 + + +def assess_context_budget( + predicted_tokens: int, + model_name: str, + llm_config: Optional[Dict] = None, + agent_id: Optional[str] = None +) -> Tuple[ContextBudgetStatus, Dict]: + """ + Assesses the current context budget consumption and returns status with metadata. + + Args: + predicted_tokens: The estimated token count for the current turn + model_name: The model identifier + llm_config: Optional LLM configuration + agent_id: Optional agent ID for logging + + Returns: + Tuple of (status, metadata_dict) where metadata includes: + - context_limit: The total context limit + - predicted_tokens: The input token count + - utilization_percent: Percentage of context used + - remaining_tokens: Estimated remaining capacity + - recommendation: Human-readable recommendation + """ + context_limit = get_model_context_limit(model_name, llm_config) + utilization = predicted_tokens / context_limit if context_limit > 0 else 1.0 + remaining = context_limit - predicted_tokens + + metadata = { + "context_limit": context_limit, + "predicted_tokens": predicted_tokens, + "utilization_percent": round(utilization * 100, 1), + "remaining_tokens": max(0, remaining), + "model_name": model_name, + "agent_id": agent_id + } + + if utilization >= EXCEEDED_THRESHOLD: + status = ContextBudgetStatus.EXCEEDED + metadata["recommendation"] = "EMERGENCY: Context limit exceeded. Request will likely fail." + logger.error("context_budget_exceeded", extra=metadata) + + elif utilization >= CRITICAL_THRESHOLD: + status = ContextBudgetStatus.CRITICAL + metadata["recommendation"] = "CRITICAL: Must complete immediately. Call generate_message_summary now." + logger.warning("context_budget_critical", extra=metadata) + + elif utilization >= WARNING_THRESHOLD: + status = ContextBudgetStatus.WARNING + metadata["recommendation"] = "WARNING: Context budget running low. Begin wrapping up your work." + logger.info("context_budget_warning", extra=metadata) + + else: + status = ContextBudgetStatus.HEALTHY + metadata["recommendation"] = "Healthy context budget. Continue normal operation." + logger.debug("context_budget_healthy", extra=metadata) + + return status, metadata + + +def generate_context_budget_directive( + status: ContextBudgetStatus, + metadata: Dict, + agent_type: Optional[str] = None +) -> Optional[str]: + """ + Generates a system directive to inject based on context budget status. + + For HEALTHY status, returns None (no injection needed). + For WARNING/CRITICAL/EXCEEDED, returns a directive to guide the agent. + + Args: + status: The current ContextBudgetStatus + metadata: Metadata from assess_context_budget + agent_type: The agent type ("principal", "partner", "associate", etc.) + + Returns: + A directive string to inject, or None if no injection needed + """ + if status == ContextBudgetStatus.HEALTHY: + return None + + utilization = metadata.get("utilization_percent", 0) + remaining = metadata.get("remaining_tokens", 0) + + # Select appropriate tool name based on agent type + if agent_type in ("principal", "partner"): + wrap_up_tool = "finish_flow" + wrap_up_action = "conclude your analysis and finalize results" + else: + wrap_up_tool = "generate_message_summary" + wrap_up_action = "summarize your findings and submit your deliverable" + + if status == ContextBudgetStatus.WARNING: + return f""" +⚠️ **CONTEXT BUDGET WARNING** ⚠️ + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**Action Required:** +- Begin consolidating your findings +- Avoid making additional large queries +- Plan to call `{wrap_up_tool}` within the next 1-2 turns +- If you have sufficient information, call `{wrap_up_tool}` NOW + +Continue with your current task but prioritize completion. +""" + + elif status == ContextBudgetStatus.CRITICAL: + return f""" +🚨 **CRITICAL: CONTEXT BUDGET EXHAUSTED** 🚨 + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**MANDATORY ACTION:** +You MUST call `{wrap_up_tool}` tool IMMEDIATELY to {wrap_up_action}. + +Do NOT make any more search or query tool calls. Any additional queries will cause a system failure. + +{wrap_up_action.capitalize()} NOW. +""" + + elif status == ContextBudgetStatus.EXCEEDED: + return f""" +πŸ›‘ **EMERGENCY: CONTEXT LIMIT EXCEEDED** πŸ›‘ + +Your context utilization is at {utilization}% - the system is at risk of failure. + +**EMERGENCY ACTION:** +Call `{wrap_up_tool}` IMMEDIATELY to {wrap_up_action}. + +Your response must be MINIMAL. Include only: +1. The most important finding +2. A note that full analysis was interrupted due to context limits + +This is your FINAL opportunity to submit work before system failure. +""" + + return None + + +def should_force_tool_call(status: ContextBudgetStatus, agent_type: Optional[str] = None) -> Optional[str]: + """ + Determines if a forced tool call should be injected. + + At CRITICAL or EXCEEDED status, we may want to programmatically + force the agent to call the summary tool rather than relying on + the directive alone. + + Args: + status: The current ContextBudgetStatus + agent_type: The agent type ("principal", "partner", "associate", etc.) + + Returns: + Tool name to force, or None if no forced call needed + """ + if status in (ContextBudgetStatus.CRITICAL, ContextBudgetStatus.EXCEEDED): + # Principal and Partner agents use finish_flow to wrap up + # Associates use generate_message_summary to submit deliverables + if agent_type in ("principal", "partner"): + return "finish_flow" + return "generate_message_summary" + return None + + +def synthesize_partial_results( + team_state: Dict, + triggered_agent_id: Optional[str] = None, + budget_metadata: Optional[Dict] = None +) -> Dict: + """ + Synthesize partial results when circuit breaker fires. + + Instead of returning nothing when the context budget is exceeded, + this function compiles available work from completed modules into + an actionable summary that can be returned to the user. + + Args: + team_state: The team state containing work_modules + triggered_agent_id: ID of the agent that triggered the circuit breaker + budget_metadata: Metadata from the budget assessment + + Returns: + A structured report containing: + - What was requested (original question) + - What was completed (modules with deliverables) + - What remains incomplete (in-progress or pending modules) + - Key findings extracted from deliverables + """ + work_modules = team_state.get("work_modules", {}) + original_question = team_state.get("question", "Unknown question") + + completed_modules = [] + incomplete_modules = [] + key_findings = [] + tools_used_overall = set() + + for module_id, module in work_modules.items(): + status = module.get("status", "unknown") + objective = module.get("objective", "Unknown objective") + assigned_agent = module.get("assigned_agent_id", "Unassigned") + + if status in ("completed", "done", "COMPLETED"): + deliverables = module.get("deliverables", {}) + + completed_modules.append({ + "module_id": module_id, + "objective": objective, + "agent": assigned_agent, + "has_deliverables": bool(deliverables), + "deliverable_summary": _extract_deliverable_summary(deliverables) + }) + + # Extract key findings from deliverables + findings = _extract_key_findings(deliverables) + if findings: + key_findings.extend(findings) + + # Track tools used + tools = module.get("tools_used", []) + if isinstance(tools, list): + tools_used_overall.update(tools) + else: + incomplete_modules.append({ + "module_id": module_id, + "objective": objective, + "status": status, + "agent": assigned_agent + }) + + # Build the synthesis report + utilization = budget_metadata.get("utilization_percent", 0) if budget_metadata else 0 + + synthesis = { + "circuit_breaker_triggered": True, + "triggered_by": triggered_agent_id, + "original_question": original_question[:500] + "..." if len(original_question) > 500 else original_question, + "context_utilization_at_trigger": f"{utilization:.1f}%", + "summary": { + "total_modules_planned": len(work_modules), + "modules_completed": len(completed_modules), + "modules_incomplete": len(incomplete_modules), + "tools_employed": list(tools_used_overall)[:10] # Limit to 10 tools + }, + "completed_work": completed_modules, + "incomplete_work": incomplete_modules, + "key_findings": key_findings[:10], # Limit to top 10 findings + "user_message": _generate_user_message( + completed_modules, + incomplete_modules, + key_findings, + utilization + ) + } + + logger.info("circuit_breaker_synthesis_generated", extra={ + "triggered_by": triggered_agent_id, + "completed_modules": len(completed_modules), + "incomplete_modules": len(incomplete_modules), + "key_findings_count": len(key_findings) + }) + + return synthesis + + +def _extract_deliverable_summary(deliverables: Dict) -> str: + """Extract a brief summary from deliverables dict.""" + if not deliverables: + return "No deliverables captured" + + if isinstance(deliverables, str): + return deliverables[:200] + "..." if len(deliverables) > 200 else deliverables + + if isinstance(deliverables, dict): + # Look for common summary keys + for key in ("summary", "main_finding", "conclusion", "result", "answer"): + if key in deliverables: + val = str(deliverables[key]) + return val[:200] + "..." if len(val) > 200 else val + + # Fall back to listing available keys + keys = list(deliverables.keys())[:5] + return f"Contains: {', '.join(keys)}" + + return str(deliverables)[:200] + + +def _extract_key_findings(deliverables: Dict) -> list: + """Extract key findings from deliverables for synthesis.""" + findings = [] + + if not deliverables or not isinstance(deliverables, dict): + return findings + + # Look for findings/conclusions/recommendations + finding_keys = ("findings", "key_findings", "conclusions", "recommendations", + "insights", "summary", "main_points") + + for key in finding_keys: + if key in deliverables: + value = deliverables[key] + if isinstance(value, list): + findings.extend([str(f)[:200] for f in value[:3]]) # First 3 items + elif isinstance(value, str): + findings.append(value[:200]) + elif isinstance(value, dict): + findings.append(str(value)[:200]) + + return findings + + +def _generate_user_message( + completed: list, + incomplete: list, + findings: list, + utilization: float +) -> str: + """Generate a user-friendly message about the partial results.""" + parts = [] + + parts.append(f"**Note: Work was interrupted at {utilization:.0f}% context utilization.**\n") + + if completed: + parts.append(f"βœ… **Completed**: {len(completed)} work module(s)") + for mod in completed[:3]: # Show first 3 + parts.append(f" - {mod['objective'][:80]}") + + if incomplete: + parts.append(f"\n⏳ **Incomplete**: {len(incomplete)} work module(s) remain") + for mod in incomplete[:3]: + parts.append(f" - {mod['objective'][:80]} (Status: {mod['status']})") + + if findings: + parts.append("\nπŸ“Œ **Key Findings So Far:**") + for finding in findings[:5]: + parts.append(f" - {finding[:150]}") + + if incomplete: + parts.append("\n*The incomplete work can be resumed in a follow-up session.*") + + return "\n".join(parts) + + +class ContextBudgetGuardian: + """ + Stateful guardian that tracks context consumption across an agent's lifetime. + + This class can be attached to an agent to provide turn-over-turn tracking + and trend analysis. + """ + + def __init__( + self, + model_name: str, + llm_config: Optional[Dict] = None, + agent_id: Optional[str] = None, + agent_type: Optional[str] = None + ): + self.model_name = model_name + self.llm_config = llm_config + self.agent_id = agent_id + self.agent_type = agent_type + self.context_limit = get_model_context_limit(model_name, llm_config) + self.turn_history: list = [] + self.warnings_issued = 0 + self.critical_issued = False + + def record_turn(self, predicted_tokens: int) -> Tuple[ContextBudgetStatus, Dict]: + """ + Records a turn's token consumption and returns the assessment. + + Args: + predicted_tokens: Token count for this turn + + Returns: + Tuple of (status, metadata) + """ + status, metadata = assess_context_budget( + predicted_tokens=predicted_tokens, + model_name=self.model_name, + llm_config=self.llm_config, + agent_id=self.agent_id + ) + + self.turn_history.append({ + "predicted_tokens": predicted_tokens, + "status": status.name, + "utilization_percent": metadata["utilization_percent"] + }) + + # Track escalation + if status == ContextBudgetStatus.WARNING: + self.warnings_issued += 1 + elif status in (ContextBudgetStatus.CRITICAL, ContextBudgetStatus.EXCEEDED): + self.critical_issued = True + + # Add trend info to metadata + metadata["turn_count"] = len(self.turn_history) + metadata["warnings_issued"] = self.warnings_issued + metadata["critical_issued"] = self.critical_issued + + if len(self.turn_history) >= 2: + prev_util = self.turn_history[-2]["utilization_percent"] + curr_util = metadata["utilization_percent"] + metadata["utilization_trend"] = curr_util - prev_util + metadata["trend_direction"] = "increasing" if curr_util > prev_util else "stable_or_decreasing" + + return status, metadata + + def get_directive(self, status: ContextBudgetStatus, metadata: Dict) -> Optional[str]: + """ + Gets the appropriate directive based on status and history. + + May adjust directive based on whether warnings have already been issued. + """ + directive = generate_context_budget_directive(status, metadata, agent_type=self.agent_type) + + # If we've already issued warnings but agent hasn't wrapped up, escalate language + if directive and self.warnings_issued > 2 and status == ContextBudgetStatus.WARNING: + directive = directive.replace( + "Continue with your current task but prioritize completion.", + "You have received multiple warnings. PRIORITIZE COMPLETION NOW." + ) + + return directive diff --git a/core/agent_core/framework/tool_registry.py b/core/agent_core/framework/tool_registry.py index 7a41c69..0d00abe 100644 --- a/core/agent_core/framework/tool_registry.py +++ b/core/agent_core/framework/tool_registry.py @@ -74,7 +74,7 @@ def decorator(cls): from pocketflow import BaseNode if not (inspect.isclass(cls) and issubclass(cls, BaseNode)): raise TypeError(f"@tool_registry can only be applied to subclasses of BaseNode, not {cls}") - + actual_toolset_name = toolset_name if toolset_name else name tool_info = { @@ -91,16 +91,16 @@ def decorator(cls): "source_uri_field_in_output": source_uri_field_in_output, "title_field_in_output": title_field_in_output } - + if name in _TOOL_REGISTRY: logger.warning("tool_registration_overwrite", extra={"description": "Tool name already exists and will be overwritten", "tool_name": name}) _TOOL_REGISTRY[name] = tool_info - + cls._tool_info = tool_info - + logger.debug("tool_registered", extra={"description": "Registered tool", "tool_name": name, "toolset_name": actual_toolset_name, "ends_flow": ends_flow}) return cls - + return decorator def get_registered_tools(): @@ -128,9 +128,9 @@ def get_tools_by_toolset_names(toolset_names: List[str]) -> List[Dict]: """ if not toolset_names: return [] - + toolset_names_set = set(toolset_names) - + return [ tool_info for tool_info in get_registered_tools() if tool_info.get("toolset_name") in toolset_names_set @@ -139,7 +139,7 @@ def get_tools_by_toolset_names(toolset_names: List[str]) -> List[Dict]: def get_all_toolsets_with_tools() -> Dict[str, List[Dict]]: """ Gets all toolsets and their associated tool information. - + Returns: A dictionary where keys are toolset names and values are lists of tool information for that toolset. @@ -151,9 +151,9 @@ def get_all_toolsets_with_tools() -> Dict[str, List[Dict]]: toolset_name = tool_info.get("toolset_name", tool_info["name"]) if toolset_name not in toolsets_data: toolsets_data[toolset_name] = [] - + final_description_for_client = tool_info.get("description", "") - + toolsets_data[toolset_name].append({ "name": tool_info["name"], "description": final_description_for_client, @@ -165,28 +165,28 @@ def format_tools_for_llm_api(tools_list: List[Dict]) -> List[Dict]: """ Formats a list of tools into the format required by the LLM API, appending toolset information to the description and sanitizing the schema. - + Args: tools_list: A list of tool information dictionaries. - + Returns: A list of tools formatted for the LLM API. """ api_tools = [] for tool_info in tools_list: description = tool_info.get("description", "") - + toolset_name = tool_info.get("toolset_name", tool_info["name"]) description_with_toolset = f"{description} (Belongs to toolset: '{toolset_name}')" - + parameters = tool_info.get("parameters", {}) if not isinstance(parameters, dict): logger.warning("tool_invalid_parameters", extra={"description": "Tool has non-dict parameters; using empty object", "tool_name": tool_info.get('name', 'unknown'), "parameters": str(parameters)}) parameters = {"type": "object", "properties": {}} - + # Sanitize the schema to remove all custom fields starting with 'x-' before sending to the API sanitized_parameters = _sanitize_schema_for_api(parameters) - + api_tool = { "type": "function", "function": { @@ -201,30 +201,30 @@ def format_tools_for_llm_api(tools_list: List[Dict]) -> List[Dict]: def format_tools_for_prompt(tools_list) -> str: """ Formats a list of tools into a string for system prompts. - + Args: tools_list: A list of tool information dictionaries. - + Returns: A string describing the tools for a system prompt. """ formatted_text = "### Registered Tools\n\n" - + for tool in tools_list: name = tool.get("name", "") description = tool.get("description", "") - + formatted_text += f"**{name}**: {description}\n\n" - + return formatted_text def format_tools_for_prompt_by_toolset(tools_by_toolset: Dict[str, List[Dict]]) -> str: """ Formats a dictionary of tools grouped by toolset into a prompt string. - + Args: tools_by_toolset: A dictionary of toolsets and their tools. - + Returns: A string describing the tools, grouped by toolset, for a system prompt. """ @@ -241,7 +241,7 @@ def format_tools_for_prompt_by_toolset(tools_by_toolset: Dict[str, List[Dict]]) for tool_info in tools_in_set: name = tool_info.get("name", "") description = tool_info.get("description", "") - + prompt_parts.append(f"* **{name}**: {description}\n") prompt_parts.append("\n") @@ -250,10 +250,10 @@ def format_tools_for_prompt_by_toolset(tools_by_toolset: Dict[str, List[Dict]]) def format_simplified_tools_for_prompt_by_toolset(tools_by_toolset: Dict[str, List[Dict]]) -> str: """ Formats tools grouped by toolset into a simplified prompt string (no parameters). - + Args: tools_by_toolset: A dictionary of toolsets and their tools. - + Returns: A simplified Markdown list of tools for a system prompt. """ @@ -270,7 +270,7 @@ def format_simplified_tools_for_prompt_by_toolset(tools_by_toolset: Dict[str, Li for tool_info in tools_in_set: name = tool_info.get("name", "") description = tool_info.get("description", "") - + prompt_parts.append(f"* **{name}**: {description}\n") prompt_parts.append("\n") @@ -357,7 +357,7 @@ def _cache_mcp_tools_to_yaml(): for tool_name, tool_info in _TOOL_REGISTRY.items(): if tool_info.get("implementation_type") == "native_mcp": description = tool_info.get("description", "No description available.") - + mcp_tools_to_cache[tool_name] = description if not mcp_tools_to_cache: @@ -412,7 +412,7 @@ async def initialize_registry(discovery_session_group: Optional[ClientSessionGro if module_name in sys.modules: logger.debug("module_force_reload", extra={"description": "Removing module from sys.modules to force reload", "module_name": module_name}) del sys.modules[module_name] - + importlib.import_module(module_name) logger.debug("custom_tool_module_imported", extra={"description": "Successfully imported custom tool module", "module_name": module_name}) except ImportError as e: @@ -433,7 +433,7 @@ async def initialize_registry(discovery_session_group: Optional[ClientSessionGro protocol_schema = HandoverService.get_protocol_schema(protocol_name) if protocol_schema: # Deep copy to avoid modifying the original object - merged_params = copy.deepcopy(tool_info["parameters"]) + merged_params = copy.deepcopy(tool_info["parameters"]) # Heuristic: Try to find a nested 'items' for array-based tools (like dispatch_submodules) # This allows handover parameters to be defined once in YAML and merged into the correct location. @@ -443,12 +443,12 @@ async def initialize_registry(discovery_session_group: Optional[ClientSessionGro if isinstance(prop_value, dict) and prop_value.get('type') == 'array' and 'items' in prop_value and isinstance(prop_value.get('items'), dict) and 'properties' in prop_value['items']: target_schema_for_merge = prop_value['items'] logger.debug("handover_protocol_nested_array_found", extra={"description": "Found nested array, will merge handover params into its 'items' schema"}) - break + break # Merge properties from protocol into the target schema target_schema_for_merge.setdefault("properties", {}).update( protocol_schema.get("properties", {}) - ) + ) # Merge required fields from protocol into the target schema req_list = target_schema_for_merge.setdefault("required", []) req_set = set(req_list) @@ -466,7 +466,7 @@ async def initialize_registry(discovery_session_group: Optional[ClientSessionGro for session in discovery_session_group.sessions: server_name = getattr(session, 'server_name_from_config', None) - + if not server_name: logger.warning("mcp_session_unnamed_skip", extra={"description": "Found a connected but unnamed MCP session, skipping tool discovery", "session": str(session)}) continue @@ -499,36 +499,151 @@ async def initialize_registry(discovery_session_group: Optional[ClientSessionGro return _TOOL_REGISTRY +def get_all_mcp_server_toolset_names() -> List[str]: + """ + Returns a list of all unique MCP server names that have registered tools. + This enables auto-discovery of available MCP toolsets. + """ + mcp_server_names = set() + for tool_info in _TOOL_REGISTRY.values(): + if tool_info.get("implementation_type") == "native_mcp": + server_name = tool_info.get("mcp_server_name") + if server_name: + mcp_server_names.add(server_name) + return sorted(list(mcp_server_names)) + + +def get_mcp_servers_by_category(category: str) -> List[str]: + """ + Returns a list of ENABLED MCP server names that belong to the specified category. + + Categories are defined in mcp.json via the 'category' field on each server. + Only servers that are both enabled AND have an EXPLICIT matching category are returned. + + STRICT MATCHING: Servers without an explicit 'category' field in mcp.json + default to 'uncategorized' and will NOT be matched by this function for + 'google_related' or 'user_specified' queries. + + Standard categories: + - "google_related": Google/Gemini related servers (e.g., "G" for Gemini CLI) + - "user_specified": User-defined domain-specific servers (must be explicitly set) + - "uncategorized": Default for servers without explicit category (not matched by category toolsets) + + Args: + category: The category name to filter by + + Returns: + List of server names that are enabled and explicitly match the category + """ + from agent_core.config.app_config import get_mcp_server_categories + + mcp_server_categories = get_mcp_server_categories() + matching_servers = [] + registered_mcp_servers = set(get_all_mcp_server_toolset_names()) + + for server_name, server_info in mcp_server_categories.items(): + if (server_info.get("category") == category and + server_info.get("enabled", False) and + server_name in registered_mcp_servers): + matching_servers.append(server_name) + + logger.debug("mcp_servers_by_category", extra={ + "category": category, + "matching_servers": matching_servers, + "all_categories": dict(mcp_server_categories) + }) + return sorted(matching_servers) + + +def expand_toolset_category(toolset_name: str) -> List[str]: + """ + Expands a toolset category name into actual MCP server names. + + Special category toolset names (resolved dynamically based on mcp.json): + - "*" or "all_mcp_servers": All registered (and enabled) MCP server toolsets + - "all_google_related_mcp_servers": Only enabled servers with EXPLICIT category="google_related" + - "all_user_specified_mcp_servers": Only enabled servers with EXPLICIT category="user_specified" + + IMPORTANT: Category matching is STRICT. Servers without an explicit 'category' + field in mcp.json default to 'uncategorized' and will NOT be matched by + 'all_google_related_mcp_servers' or 'all_user_specified_mcp_servers'. + + To add a new user server that should be included in 'all_user_specified_mcp_servers': + 1. Add the server to mcp.json + 2. Set "category": "user_specified" explicitly + 3. Set "enabled": true + + Args: + toolset_name: The toolset name, which may be a category + + Returns: + List of actual server names, or [toolset_name] if not a category + """ + if toolset_name in ("*", "all_mcp_servers"): + return get_all_mcp_server_toolset_names() + elif toolset_name == "all_google_related_mcp_servers": + return get_mcp_servers_by_category("google_related") + elif toolset_name == "all_user_specified_mcp_servers": + return get_mcp_servers_by_category("user_specified") + else: + return [toolset_name] + + def get_tools_for_profile(loaded_profile: Dict, context: Dict, agent_id: str) -> List[Dict]: """ Gets the list of tools available to an agent based on its loaded profile and the current context. Tool access is governed by the profile's `tool_access_policy` and any overrides from the Principal. + + Special toolset names (category-based, resolved dynamically): + - "*" or "all_mcp_servers": All registered (and enabled) MCP server toolsets + - "all_google_related_mcp_servers": Only enabled servers with category="google_related" + - "all_user_specified_mcp_servers": Only enabled servers with category="user_specified" """ sub_context_state = context["state"] profile_id = loaded_profile.get("profile_id", "UnknownProfile") - + logger.debug("agent_tools_profile_evaluation", extra={"description": "Determining tools based on profile's tool_access_policy", "agent_id": agent_id, "profile_id": profile_id}) - + is_associate_agent = "Associate" in agent_id - + final_tools_list = [] processed_tool_names = set() tool_access_policy = loaded_profile.get("tool_access_policy", {}) profile_allowed_toolsets = tool_access_policy.get("allowed_toolsets", []) profile_allowed_individual_tools = tool_access_policy.get("allowed_individual_tools", []) - logger.debug("agent_profile_tool_policy", extra={"description": "Profile's tool access policy", "agent_id": agent_id, "profile_id": profile_id, "allowed_toolsets": profile_allowed_toolsets, "allowed_individual_tools": profile_allowed_individual_tools}) + + # Expand category-based toolsets (e.g., "all_user_specified_mcp_servers" -> ["Seats", ...]) + # This is explicit opt-in: profiles must specify which categories they want access to + expanded_toolsets = [] + for ts in profile_allowed_toolsets: + expanded = expand_toolset_category(ts) + if expanded != [ts]: + logger.info("expanded_toolset_category", extra={ + "description": "Expanded toolset category to actual servers", + "agent_id": agent_id, + "category": ts, + "expanded_to": expanded + }) + expanded_toolsets.extend(expanded) + + profile_allowed_toolsets = expanded_toolsets + + logger.debug("agent_profile_tool_policy", extra={"description": "Profile's tool access policy (expanded)", "agent_id": agent_id, "profile_id": profile_id, "allowed_toolsets": profile_allowed_toolsets, "allowed_individual_tools": profile_allowed_individual_tools}) # Check for Principal-specified toolset override for Associates from the state within the SubContext principal_override_toolsets_for_associate = sub_context_state.get("allowed_toolsets") - candidate_tool_sources = [] + candidate_tool_sources = [] if is_associate_agent and principal_override_toolsets_for_associate is not None: logger.info("agent_principal_toolset_override", extra={"description": "Using Principal-specified toolsets override", "agent_id": agent_id, "profile_id": profile_id, "override_toolsets": principal_override_toolsets_for_associate}) - # If principal_override_toolsets_for_associate is an empty list [], it means NO registry tools. - for toolset_name in principal_override_toolsets_for_associate: + # Expand category-based toolsets in override too + expanded_override = [] + for ts in principal_override_toolsets_for_associate: + expanded_override.extend(expand_toolset_category(ts)) + for toolset_name in expanded_override: tools_in_set = get_tools_by_toolset_names([toolset_name]) logger.debug("agent_override_toolset_tools", extra={"description": "Tools found for overridden toolset", "agent_id": agent_id, "profile_id": profile_id, "toolset_name": toolset_name, "tool_names": [t['name'] for t in tools_in_set]}) candidate_tool_sources.append( (f"toolset '{toolset_name}' from Principal override", tools_in_set) ) @@ -541,7 +656,7 @@ def get_tools_for_profile(loaded_profile: Dict, context: Dict, agent_id: str) -> tools_in_set = get_tools_by_toolset_names([toolset_name]) logger.debug("agent_toolset_tools_found", extra={"description": "Tools found for toolset", "agent_id": agent_id, "profile_id": profile_id, "toolset_name": toolset_name, "tool_names": [t['name'] for t in tools_in_set]}) candidate_tool_sources.append( (f"toolset '{toolset_name}' from profile", tools_in_set) ) - + if profile_allowed_individual_tools: logger.debug("agent_individual_tools_processing", extra={"description": "Processing profile's allowed_individual_tools", "agent_id": agent_id, "profile_id": profile_id, "allowed_individual_tools": profile_allowed_individual_tools}) individual_tool_infos = [] @@ -554,12 +669,12 @@ def get_tools_for_profile(loaded_profile: Dict, context: Dict, agent_id: str) -> logger.warning("agent_individual_tool_not_found", extra={"description": "Individual tool from profile not found in registry", "agent_id": agent_id, "profile_id": profile_id, "tool_name": tool_name}) if individual_tool_infos: candidate_tool_sources.append( (f"individual tools from profile", individual_tool_infos) ) - + logger.debug("agent_candidate_tool_sources", extra={"description": "Candidate tool sources", "agent_id": agent_id, "profile_id": profile_id, "sources": [(s[0], [t['name'] for t in s[1]]) for s in candidate_tool_sources]}) # Process all candidate tools, ensuring no duplicates. Scope filtering is removed. for source_desc, tools_from_source in candidate_tool_sources: - for tool_info in tools_from_source: + for tool_info in tools_from_source: tool_name = tool_info["name"] if tool_name not in processed_tool_names: # Scope check removed. If a tool is in candidate_tool_sources, it's considered applicable based on profile. @@ -569,7 +684,7 @@ def get_tools_for_profile(loaded_profile: Dict, context: Dict, agent_id: str) -> # else: logger.debug(f"Tool '{tool_name}' from {source_desc} already processed.") # Client-declared MCP tools are NO LONGER PROCESSED HERE as per V5 plan. - + logger.debug("agent_final_tools_determined", extra={"description": "Final applicable tools determined", "agent_id": agent_id, "profile_id": profile_id, "tool_count": len(final_tools_list), "tool_names": [t['name'] for t in final_tools_list]}) return final_tools_list @@ -589,16 +704,19 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): agent_operational_scope_for_log = "principal" if "Principal" in node.agent_id else "agent" if hasattr(node, 'agent_id') else "unknown_node_type" node_identifier_for_log = node.agent_id if hasattr(node, 'agent_id') else node.__class__.__name__ logger.debug("node_tool_connection_begin", extra={"description": "Connecting tools to node. Tool availability determined by Profile", "node_identifier": node_identifier_for_log, "node_class": node.__class__.__name__, "agent_scope": agent_operational_scope_for_log}) - - from pocketflow import Flow, AsyncFlow, Node, AsyncNode + + from pocketflow import Flow, AsyncFlow, Node, AsyncNode from ..nodes.mcp_proxy_node import MCPProxyNode - from ..nodes.base_agent_node import AgentNode + from ..nodes.base_agent_node import AgentNode tools_to_connect_definitions: List[Dict] = [] if isinstance(node, AgentNode): - if not node.loaded_profile: + if not node.loaded_profile: logger.error("agent_node_profile_not_set", extra={"description": "AgentNode profile not set prior to connect_tools_to_node. This indicates an issue with AgentNode initialization. No tools will be connected", "agent_id": node.agent_id}) - return {} + return {} + if context is None: + logger.error("agent_node_context_required", extra={"description": "Context is required for connect_tools_to_node but was None. No tools will be connected", "agent_id": node.agent_id}) + return {} # Get the definitive list of tool definitions for this AgentNode instance based on its profile. # Pass the context object to get_tools_for_profile tools_to_connect_definitions = get_tools_for_profile(node.loaded_profile, context, node.agent_id) @@ -616,28 +734,28 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): for tool_info in tools_to_connect_definitions: name = tool_info["name"] impl_type = tool_info.get("implementation_type", "internal") # Default to "internal" - + # For "internal" type, node_class should be in tool_info from @tool_registry decorator # For "internal_profile_agent", node_class is always AgentNode. # For "native_mcp", node_class is MCPProxyNode. - + node_class_from_registry = tool_info.get("node_class") # This is set for "internal" type by decorator ends_flow_tool = tool_info.get("ends_flow", False) action_name = name # PocketFlow action is the tool name - + logger.debug("tool_connection_attempt", extra={"description": "Attempting to connect tool", "tool_name": name, "implementation_type": impl_type, "ends_flow": ends_flow_tool}) - + node_instance = None - + if impl_type == "internal": if node_class_from_registry: try: if issubclass(node_class_from_registry, AgentNode): logger.error("tool_invalid_agent_node_class", extra={"description": "Tool (impl_type 'internal') has AgentNode as its class. It should be 'internal_profile_agent'. Skipping", "tool_name": name}) continue - + if issubclass(node_class_from_registry, (Flow, AsyncFlow)): - node_instance = node_class_from_registry() + node_instance = node_class_from_registry() elif issubclass(node_class_from_registry, (Node, AsyncNode)): node_instance = node_class_from_registry(max_retries=tool_info.get("max_retries",1), wait=tool_info.get("wait",1)) else: @@ -648,10 +766,10 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): except Exception as e: logger.error("internal_tool_node_instantiation_error", extra={"description": "Error instantiating 'internal' tool node", "tool_name": name, "node_class": node_class_from_registry.__name__, "error": str(e)}, exc_info=True) continue - else: + else: logger.warning("internal_tool_missing_node_class", extra={"description": "'internal' tool is missing node_class definition in registry. Skipping", "tool_name": name}) continue - + elif impl_type == "native_mcp": server_name = tool_info.get("mcp_server_name") original_name = tool_info.get("original_name") @@ -669,7 +787,7 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): max_retries=3, wait=1 ) - + tool_nodes[name] = node_instance logger.debug("mcp_proxy_node_instantiated", extra={"description": "Instantiated MCPProxyNode for tool", "unique_name": unique_name, "original_name": original_name, "server_name": server_name}) except Exception as e: @@ -678,15 +796,15 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): else: logger.warning("tool_unknown_implementation_type", extra={"description": "Tool has unknown implementation type", "tool_name": name, "implementation_type": impl_type}) continue - + if node_instance: try: if not hasattr(node, 'successors'): setattr(node, 'successors', {}) - + node.next(node_instance, action=action_name) logger.debug("tool_node_connected", extra={"description": "Connected tool node", "source_node": node.__class__.__name__, "action_name": action_name, "target_node": node_instance.__class__.__name__}) - + if not ends_flow_tool: if not hasattr(node_instance, 'successors'): setattr(node_instance, 'successors', {}) @@ -697,9 +815,9 @@ def connect_tools_to_node(node, context: Optional[Dict] = None): except Exception as e: logger.error("tool_node_connection_error", extra={"description": "Error connecting nodes for tool", "tool_name": name, "error": str(e)}, exc_info=True) - + except Exception as e: logger.error("tool_connection_general_error", extra={"description": "General error during tool connection for node", "node_identifier": node_identifier_for_log, "error": str(e)}, exc_info=True) - + logger.debug("node_tool_connection_complete", extra={"description": "Finished connecting tools for node", "node_identifier": node_identifier_for_log, "connected_tool_count": len(tool_nodes)}) return tool_nodes diff --git a/core/agent_core/iic/core/event_handler.py b/core/agent_core/iic/core/event_handler.py index 73cdcbc..d163bc9 100644 --- a/core/agent_core/iic/core/event_handler.py +++ b/core/agent_core/iic/core/event_handler.py @@ -41,7 +41,7 @@ async def _save_minimal_iic_file(run_context: dict, iic_path: str): }, content="" ) - + import aiofiles async with aiofiles.open(iic_path, 'w', encoding='utf-8') as f: await f.write(root_block.to_iic()) @@ -77,7 +77,7 @@ async def _trigger_intelligent_naming(self, run_id: str, initial_text: str): slug = re.sub(r'[\s_]+', '-', slug) if not slug: slug = f"run-summary-{run_id[:4]}" - + logger.info("run_using_user_provided_filename", extra={"run_id": run_id, "initial_filename": initial_filename, "slug": slug}) await self.rename_iic_file(run_id, slug) return # Important: exit after using the provided name @@ -88,14 +88,14 @@ async def _trigger_intelligent_naming(self, run_id: str, initial_text: str): prompt = f"Please summarize the following user query into a concise, 4-5 word, filename-friendly English phrase (in slug format). Return only the filename, for example: 'research-ai-ethics-2024'. Query: '{initial_text}'" messages = [{"role": "user", "content": prompt}] - + # 1. Resolve fast_utils_llm config resolver = LLMConfigResolver(shared_llm_configs=SHARED_LLM_CONFIGS) fast_utils_llm_config = resolver.resolve({"llm_config_ref": "fast_utils_llm"}) # 2. Call LLM response = await call_litellm_acompletion( - messages=messages, + messages=messages, llm_config=fast_utils_llm_config, stream=False ) @@ -110,7 +110,7 @@ async def _trigger_intelligent_naming(self, run_id: str, initial_text: str): slug = f"run-summary-{run_id[:4]}" logger.info("llm_proposed_run_name", extra={"run_id": run_id, "proposed_name": proposed_name, "slug": slug}) - + # Call the atomic rename method await self.rename_iic_file(run_id, slug) @@ -171,7 +171,7 @@ async def rename_iic_file(self, run_id: str, new_name_slug: str): # The new_name_slug here is the display name needed by the frontend (without .iic). try: asyncio.create_task(broadcast_project_structure_update( - "rename_run", + "rename_run", {"run_id": run_id, "new_name": new_name_slug} )) logger.info("project_structure_update_broadcast_triggered", extra={"run_id": run_id}) @@ -194,7 +194,7 @@ async def sync_run_to_iic(self, run_id): if context is None: logger.error("context_not_found_for_run", extra={"run_id": run_id}) return - + # --- Update project index --- try: project_id = context.get("project_id", "default") @@ -229,7 +229,7 @@ async def _initialize_run_persistence_if_needed(self, run_id: str) -> bool: if not context: logger.warning("persistence_init_failed_context_not_found", extra={"run_id": run_id}) return False - + # Check if it is a resumed run resumed_path = context.get("meta", {}).get("source_iic_path") @@ -244,7 +244,7 @@ async def _initialize_run_persistence_if_needed(self, run_id: str) -> bool: project_id = context.get("project_id", "default") iic_dir = get_iic_dir(project_id) initial_path = os.path.join(iic_dir, f"{run_id}.iic") - + self.iic_files[run_id] = initial_path self.run_locks[run_id] = asyncio.Lock() logger.info("persistence_initialized_for_new_run", extra={"run_id": run_id, "initial_path": initial_path}) @@ -253,13 +253,13 @@ async def _initialize_run_persistence_if_needed(self, run_id: str) -> bool: initial_text = context.get("team_state", {}).get("question", "") if initial_text: asyncio.create_task(self._trigger_intelligent_naming(run_id, initial_text)) - + return True async def on_message(self, message_json): """ Handle incoming messages. - + Args: message_json (dict): The message to handle. """ @@ -267,10 +267,10 @@ async def on_message(self, message_json): body = json.loads(message_json) if not body: return - + msg_type = body.get("type", "") session_id = body.get("session_id", "") - + # Extract run_id based on message type, as 'run_ready' has it nested run_id = None if msg_type == "run_ready": @@ -290,12 +290,28 @@ async def on_message(self, message_json): # Persistence logic is now triggered by 'turn_completed' if msg_type == "turn_completed": logger.debug("persistence_triggered_by_event", extra={"msg_type": msg_type, "run_id": run_id}) - + # Initialize persistence if it's the first time for this run if await self._initialize_run_persistence_if_needed(run_id): # Call the core persistence function try: await self.sync_run_to_iic(run_id) + + # Also save checkpoint for connection manager (for reconnection resilience) + try: + from api.connection_manager import connection_manager + context = active_runs_store.get(run_id) + if context: + # Save a lightweight checkpoint for quick reconnection + checkpoint_data = { + "run_id": run_id, + "status": context.get("meta", {}).get("status"), + "turn_count": len(context.get("team_state", {}).get("turns", [])), + "has_knowledge_base": bool(context.get("team_state", {}).get("knowledge_base")) + } + connection_manager.save_checkpoint(run_id, checkpoint_data) + except Exception as e: + logger.debug("checkpoint_save_skipped", extra={"run_id": run_id, "reason": str(e)}) except Exception as e: logger.error("sync_run_to_iic_failed", extra={"run_id": run_id, "error": str(e)}, exc_info=True) else: diff --git a/core/agent_core/llm/call_llm.py b/core/agent_core/llm/call_llm.py index 21bab9e..93ddbc2 100644 --- a/core/agent_core/llm/call_llm.py +++ b/core/agent_core/llm/call_llm.py @@ -45,37 +45,20 @@ def estimate_prompt_tokens( ) -> int: """ Estimates the number of tokens for a given input. Can accept a single text string or a list of messages. + + This function delegates to the provider-aware token_counter module which uses: + - Anthropic's official count_tokens API for Claude models (accurate) + - litellm's token_counter for other providers (estimation) """ - # Prioritize using the model name specified for the tokenizer - model_for_counting = model - if llm_config_for_tokenizer and llm_config_for_tokenizer.get("litellm_token_counter_model"): - model_for_counting = llm_config_for_tokenizer["litellm_token_counter_model"] - logger.debug("token_counting_model_override", extra={"model_for_counting": model_for_counting, "override_source": "litellm_token_counter_model"}) - elif not model: - logger.warning("token_estimation_no_model", extra={"model_provided": bool(model), "override_found": False, "return_value": 0}) - return 0 - - if text is not None and messages is not None: - raise ValueError("Provide either 'text' or 'messages' to estimate_prompt_tokens, not both.") - - messages_for_calc: List[Dict] = [] - - if system_prompt: - messages_for_calc.append({"role": "system", "content": system_prompt}) - - if text is not None: - messages_for_calc.append({"role": "user", "content": text}) - elif messages is not None: - messages_for_calc.extend(messages) - - if not messages_for_calc: - return 0 - - try: - return litellm.token_counter(model=model_for_counting, messages=messages_for_calc) - except Exception as e: - logger.warning("token_estimation_failed", extra={"model_for_counting": model_for_counting, "error_message": str(e), "return_value": 0}) - return 0 + from .token_counter import count_tokens + + return count_tokens( + model=model, + text=text, + messages=messages, + system_prompt=system_prompt, + llm_config=llm_config_for_tokenizer + ) class LLMResponseAggregator: """ @@ -130,7 +113,7 @@ async def process_chunk(self, chunk: Any): if not hasattr(chunk, "choices") or not chunk.choices: logger.debug("llm_chunk_no_choices", extra={"agent_id": self.agent_id}) return - + # Store model_id if available in the first chunk (or any chunk) if not self.model_id_used and hasattr(chunk, "model") and chunk.model: self.model_id_used = chunk.model @@ -168,10 +151,10 @@ async def process_chunk(self, chunk: Any): index = tc_chunk.index if hasattr(tc_chunk, "index") else 0 if index not in self.current_tool_call_chunks: self.current_tool_call_chunks[index] = {"id": None, "type": "function", "function": {"name": "", "arguments": ""}} - + if hasattr(tc_chunk, "id") and tc_chunk.id: self.current_tool_call_chunks[index]["id"] = tc_chunk.id - + if hasattr(tc_chunk, "function"): if hasattr(tc_chunk.function, "name") and tc_chunk.function.name: self.current_tool_call_chunks[index]["function"]["name"] += tc_chunk.function.name @@ -190,7 +173,7 @@ async def process_chunk(self, chunk: Any): stream_id=self.stream_id, llm_id=self.llm_model_id, contextual_data=self._get_contextual_data_for_event() ) - + def get_aggregated_response(self, messages_for_llm: List[Dict]) -> Dict: # Reconstruct full messages if needed by litellm or for logging # full_messages_history = litellm.stream_chunk_builder(self.raw_chunks, messages=messages_for_llm) @@ -210,9 +193,9 @@ def get_aggregated_response(self, messages_for_llm: List[Dict]) -> Dict: # Fallback parsing is no longer attempted - if these tags are detected, a retry should have been triggered in process_chunk. # The original fallback logic has been removed as we now treat these cases as errors that require a retry. - + logger.debug("llm_response_aggregated", extra={"agent_id": self.agent_id, "content_length": len(self.full_content), "tool_calls_count": len(tool_calls_list), "reasoning_length": len(self.full_reasoning_content)}) - + # If model_id_used was not found in chunks, try to get it from the final response object (if available) # This part depends on how call_litellm_acompletion returns the final object after stream. # For now, we rely on chunk.model. @@ -247,7 +230,7 @@ async def call_litellm_acompletion( """ app_level_max_retries = llm_config.get("max_retries", 2) last_exception = None - + final_messages = list(messages) if system_prompt_content: if final_messages and final_messages[0].get("role") == "system": @@ -259,7 +242,7 @@ async def call_litellm_acompletion( # --- KEY CHANGE: Generate a NEW stream_id for EVERY attempt --- # Use the provided one only for the very first attempt, then generate new ones. current_stream_id = (stream_id if attempt == 0 else None) or str(uuid.uuid4()) - + try: model_name = llm_config.get("model") if not model_name: @@ -273,17 +256,21 @@ async def call_litellm_acompletion( base_params["tool_choice"] = tool_choice if stream: base_params.setdefault("stream_options", {})["include_usage"] = True - + base_params = {k: v for k, v in base_params.items() if v is not None} - - FILTERED_KEYS = ["stream_id", "parent_agent_id", "wait_seconds_on_retry", "max_retries"] + + # Keys to filter out before sending to LiteLLM API + # - stream_id, parent_agent_id: internal tracking only + # - wait_seconds_on_retry, max_retries: app-level retry config + # - max_context_tokens: used by Context Budget Guardian, not a valid LiteLLM/API param + FILTERED_KEYS = ["stream_id", "parent_agent_id", "wait_seconds_on_retry", "max_retries", "max_context_tokens"] params_for_litellm = {k: v for k, v in base_params.items() if k not in FILTERED_KEYS} # Emit "start" events for this new attempt if events and agent_id_for_event and run_id_for_event: await events.emit_llm_stream_started( - run_id=run_id_for_event, agent_id=agent_id_for_event, - parent_agent_id=kwargs.get('parent_agent_id'), stream_id=current_stream_id, + run_id=run_id_for_event, agent_id=agent_id_for_event, + parent_agent_id=kwargs.get('parent_agent_id'), stream_id=current_stream_id, llm_id=model_name, contextual_data=contextual_data_for_event ) params_for_event = json.loads(json.dumps(params_for_litellm, default=str)) @@ -291,12 +278,12 @@ async def call_litellm_acompletion( run_id=run_id_for_event, agent_id=agent_id_for_event, stream_id=current_stream_id, llm_id=model_name, params=params_for_event, contextual_data=contextual_data_for_event ) - + logger.info("litellm_call_attempt", extra={"attempt": attempt + 1, "max_attempts": app_level_max_retries + 1, "model_name": model_name, "stream_id": current_stream_id}) - + # --- Direct call to litellm inside the main try block --- llm_response_stream = await litellm.acompletion(**params_for_litellm) - + response_aggregator = LLMResponseAggregator( agent_id=agent_id_for_event, parent_agent_id=kwargs.get('parent_agent_id'), @@ -313,12 +300,12 @@ async def call_litellm_acompletion( await response_aggregator.process_chunk(chunk) aggregated_response = response_aggregator.get_aggregated_response(messages_for_llm=final_messages) - + if not aggregated_response.get("content", "").strip() and not aggregated_response.get("tool_calls", []): raise FunctionCallErrorException("Received completely empty response from LLM, forcing retry.") aggregated_response['final_stream_id'] = current_stream_id - + # (Token usage and event emission logic remains the same) if run_context: stats = run_context['runtime']['token_usage_stats'] @@ -352,13 +339,13 @@ async def call_litellm_acompletion( ) as e_retry: last_exception = e_retry is_app_error = isinstance(e_retry, FunctionCallErrorException) - + logger.warning("llm_retry_triggered", extra={ - "stream_id": current_stream_id, - "reason": str(e_retry), + "stream_id": current_stream_id, + "reason": str(e_retry), "error_type": type(e_retry).__name__, "is_app_error": is_app_error, - "attempt": attempt + 1, + "attempt": attempt + 1, "max_attempts": app_level_max_retries + 1 }) @@ -377,7 +364,7 @@ async def call_litellm_acompletion( if events and agent_id_for_event and run_id_for_event: await events.emit_llm_stream_failed( run_id=run_id_for_event, agent_id=agent_id_for_event, parent_agent_id=kwargs.get('parent_agent_id'), - stream_id=current_stream_id, reason=f"Retrying due to: {type(e_retry).__name__} - {str(e_retry)}", + stream_id=current_stream_id, reason=f"Retrying due to: {type(e_retry).__name__} - {str(e_retry)}", contextual_data=contextual_data_for_event ) @@ -424,14 +411,14 @@ async def call_litellm_acompletion( # Backoff before next attempt await asyncio.sleep(llm_config.get("wait_seconds_on_retry", 3) * (attempt + 1)) continue # Go to the next iteration of the loop - + # --- UNRECOVERABLE ERRORS --- except (AuthenticationError, BadRequestError, ContextWindowExceededError) as e_unrecoverable: logger.error("llm_unrecoverable_error_in_orchestrator", extra={"error_type": type(e_unrecoverable).__name__, "error_message": str(e_unrecoverable)}, exc_info=True) if events and agent_id_for_event and run_id_for_event: await events.emit_llm_stream_failed( run_id=run_id_for_event, agent_id=agent_id_for_event, parent_agent_id=kwargs.get('parent_agent_id'), - stream_id=current_stream_id, reason=f"Unrecoverable error: {type(e_unrecoverable).__name__} - {str(e_unrecoverable)}", + stream_id=current_stream_id, reason=f"Unrecoverable error: {type(e_unrecoverable).__name__} - {str(e_unrecoverable)}", contextual_data=contextual_data_for_event ) return {"error": f"{type(e_unrecoverable).__name__}: {str(e_unrecoverable)}", "error_type": type(e_unrecoverable).__name__, "actual_usage": None, "content": None, "tool_calls": [], "reasoning": None, "model_id_used": None} @@ -440,11 +427,11 @@ async def call_litellm_acompletion( logger.warning("llm_call_cancelled", extra={"stream_id": current_stream_id}) if events and agent_id_for_event and run_id_for_event: await events.emit_llm_stream_failed( - run_id=run_id_for_event, - agent_id=agent_id_for_event, + run_id=run_id_for_event, + agent_id=agent_id_for_event, parent_agent_id=kwargs.get('parent_agent_id'), - stream_id=current_stream_id, - reason="Operation was cancelled by user request.", + stream_id=current_stream_id, + reason="Operation was cancelled by user request.", contextual_data=contextual_data_for_event ) raise @@ -454,7 +441,7 @@ async def call_litellm_acompletion( if events and agent_id_for_event and run_id_for_event: await events.emit_llm_stream_failed( run_id=run_id_for_event, agent_id=agent_id_for_event, parent_agent_id=kwargs.get('parent_agent_id'), - stream_id=current_stream_id, reason=f"Unexpected error: {type(e_final).__name__} - {str(e_final)}", + stream_id=current_stream_id, reason=f"Unexpected error: {type(e_final).__name__} - {str(e_final)}", contextual_data=contextual_data_for_event ) return {"error": f"Unexpected error: {str(e_final)}", "error_type": type(e_final).__name__, "actual_usage": None, "content": None, "tool_calls": [], "reasoning": None, "model_id_used": None} @@ -464,5 +451,5 @@ async def call_litellm_acompletion( final_error_message = f"LLM call failed after all retries. Last error: {type(last_exception).__name__} - {last_exception}" logger.error("final_llm_error_after_retries", extra={"error_message": final_error_message}, exc_info=True) return {"error": final_error_message, "error_type": type(last_exception).__name__, "actual_usage": None, "content": None, "tool_calls": [], "reasoning": None, "model_id_used": None} - + raise RuntimeError("LLM call logic finished unexpectedly.") diff --git a/core/agent_core/llm/config_resolver.py b/core/agent_core/llm/config_resolver.py index c2ccd65..7a8a46b 100644 --- a/core/agent_core/llm/config_resolver.py +++ b/core/agent_core/llm/config_resolver.py @@ -14,7 +14,7 @@ class LLMConfigResolver: def __init__(self, shared_llm_configs: Dict): """ Initializes the resolver. - + Args: shared_llm_configs (Dict): The LLM configuration store loaded from the loader, with inheritance already resolved. """ @@ -33,31 +33,52 @@ def _recursive_resolve(self, config_value: Any) -> Any: var_name = config_value.get("var") if not var_name: raise ValueError(f"'_type: from_env' directive is missing the 'var' key in config: {config_value}") - + env_value = os.getenv(var_name) if env_value is not None: # Try to convert string "true" / "false" to boolean, "null" to None if env_value.lower() == 'true': return True if env_value.lower() == 'false': return False if env_value.lower() == 'null': return None + + # Try to parse as JSON (for extra_headers and other complex types) + if env_value.strip().startswith('{') or env_value.strip().startswith('['): + try: + return json.loads(env_value) + except json.JSONDecodeError: + logger.warning("env_var_json_parse_failed", extra={ + "var_name": var_name, + "hint": "Value looks like JSON but failed to parse. Using as string." + }) + + # Try to convert numeric strings to int/float + try: + # Try int first + if '.' not in env_value and 'e' not in env_value.lower(): + return int(env_value) + # Then try float + return float(env_value) + except ValueError: + pass # Not a number, return as string + return env_value - + if "default" in config_value: return config_value["default"] - + if config_value.get("required", False): raise ValueError(f"Required environment variable '{var_name}' is not set and no default was provided.") - + return None if directive == "json_from_file": path_str = config_value.get("path") if not path_str: raise ValueError(f"'_type: json_from_file' directive is missing the 'path' key in config: {config_value}") - + if not os.path.exists(path_str): raise FileNotFoundError(f"File specified in 'json_from_file' not found: {path_str}") - + with open(path_str, 'r', encoding='utf-8') as f: return json.load(f) @@ -77,9 +98,9 @@ def resolve(self, agent_profile: Dict[str, Any]) -> Dict[str, Any]: base_config = get_active_llm_config_by_name(self.shared_llm_configs, llm_config_ref) if not base_config: raise ValueError(f"LLM Config '{llm_config_ref}' not found or is inactive.") - + raw_config = base_config.get("config", {}).copy() - + final_params = {} for key, value in raw_config.items(): try: @@ -90,7 +111,7 @@ def resolve(self, agent_profile: Dict[str, Any]) -> Dict[str, Any]: logger.error("config_key_resolution_error", extra={"key": key, "llm_config_ref": llm_config_ref, "error_message": str(e)}) # Depending on requirements, one can choose to throw an exception here or continue, leaving the configuration incomplete raise e - + if "litellm_options" in final_params: options = final_params.pop("litellm_options") if isinstance(options, dict): diff --git a/core/agent_core/llm/token_counter.py b/core/agent_core/llm/token_counter.py new file mode 100644 index 0000000..9a48977 --- /dev/null +++ b/core/agent_core/llm/token_counter.py @@ -0,0 +1,453 @@ +""" +Provider-aware token counting module. + +This module provides accurate token counting by using provider-specific APIs +when available, with fallback to litellm's estimation for other providers. + +Architecture: +- Each LLM provider can have a dedicated token counter implementation +- Provider detection uses model name patterns (from llm_config.model) +- Graceful fallback to litellm when provider API is unavailable + +Key insight: litellm uses tiktoken (OpenAI's tokenizer) as fallback for Claude 3+ +models, which can underestimate by 25-35%. Anthropic's official count_tokens API +provides accurate counts for Claude models. + +Extensibility: +- Add new provider by implementing _count_tokens_() function +- Register provider in PROVIDER_TOKEN_COUNTERS dict +- Add model detection pattern in _detect_provider() +""" + +import logging +from typing import List, Dict, Any, Optional, Callable, Tuple +from enum import Enum + +import litellm + +logger = logging.getLogger(__name__) + + +class LLMProvider(Enum): + """Supported LLM providers with dedicated token counting.""" + ANTHROPIC = "anthropic" + OPENAI = "openai" + GOOGLE = "google" + UNKNOWN = "unknown" + + +# ============================================================================ +# Provider Detection +# ============================================================================ + +def _detect_provider(model: str) -> LLMProvider: + """ + Detect the LLM provider from the model name. + + Handles various naming conventions from llm_configs: + - anthropic/claude-* β†’ Anthropic + - claude-* β†’ Anthropic + - bedrock/anthropic.claude-* β†’ Anthropic + - gpt-*, openai/* β†’ OpenAI + - gemini-*, google/* β†’ Google + """ + if not model: + return LLMProvider.UNKNOWN + + model_lower = model.lower() + + # Anthropic detection + if (model_lower.startswith("claude") or + "anthropic/" in model_lower or + "/claude" in model_lower or + "anthropic.claude" in model_lower): + return LLMProvider.ANTHROPIC + + # OpenAI detection + if (model_lower.startswith("gpt") or + model_lower.startswith("o1") or + model_lower.startswith("o3") or + "openai/" in model_lower or + model_lower.startswith("text-embedding")): + return LLMProvider.OPENAI + + # Google detection + if (model_lower.startswith("gemini") or + "google/" in model_lower or + "vertex_ai/" in model_lower): + return LLMProvider.GOOGLE + + return LLMProvider.UNKNOWN + + +def _normalize_model_name(model: str, provider: LLMProvider) -> str: + """ + Normalize model name by stripping provider prefixes. + + Converts litellm-style prefixes to bare model names for provider APIs. + """ + if not model: + return model + + # Common prefixes to strip based on provider + prefix_map = { + LLMProvider.ANTHROPIC: ["anthropic/", "bedrock/anthropic.", "vertex_ai/"], + LLMProvider.OPENAI: ["openai/", "azure/"], + LLMProvider.GOOGLE: ["google/", "vertex_ai/", "gemini/"], + } + + prefixes = prefix_map.get(provider, []) + normalized = model + + for prefix in prefixes: + if normalized.lower().startswith(prefix.lower()): + normalized = normalized[len(prefix):] + break + + return normalized + + +# ============================================================================ +# Anthropic Token Counter +# ============================================================================ + +_anthropic_client = None + + +def _get_anthropic_client(): + """Lazily initialize and cache the Anthropic client.""" + global _anthropic_client + if _anthropic_client is None: + try: + import anthropic + _anthropic_client = anthropic.Anthropic() + except ImportError: + logger.warning("anthropic_sdk_not_installed", extra={ + "message": "anthropic package not installed, falling back to litellm estimation" + }) + return None + except Exception as e: + logger.warning("anthropic_client_init_failed", extra={ + "error": str(e), + "message": "Failed to initialize Anthropic client, falling back to litellm" + }) + return None + return _anthropic_client + + +def _convert_messages_for_anthropic( + messages: List[Dict], + system_prompt: Optional[str] = None +) -> Tuple[Optional[str], List[Dict]]: + """ + Convert messages to Anthropic's expected format. + + Anthropic expects: + - system as a separate parameter (not in messages) + - messages with role: 'user' or 'assistant' only + - tool_use and tool_result blocks handled specially + """ + anthropic_messages = [] + extracted_system = system_prompt + + for msg in messages: + role = msg.get("role", "") + content = msg.get("content", "") + + if role == "system": + if extracted_system: + extracted_system = f"{extracted_system}\n\n{content}" + else: + extracted_system = content + continue + + if role in ("user", "assistant"): + anthropic_msg = {"role": role, "content": content} + + # Handle tool_calls (assistant response with tool use) + if "tool_calls" in msg and msg["tool_calls"]: + content_blocks = [] + if content: + content_blocks.append({"type": "text", "text": content}) + + for tc in msg["tool_calls"]: + tool_use_block = { + "type": "tool_use", + "id": tc.get("id", ""), + "name": tc.get("function", {}).get("name", ""), + "input": tc.get("function", {}).get("arguments", {}) + } + if isinstance(tool_use_block["input"], str): + try: + import json + tool_use_block["input"] = json.loads(tool_use_block["input"]) + except: + tool_use_block["input"] = {} + content_blocks.append(tool_use_block) + + anthropic_msg["content"] = content_blocks + + anthropic_messages.append(anthropic_msg) + + elif role == "tool": + tool_result_block = { + "type": "tool_result", + "tool_use_id": msg.get("tool_call_id", ""), + "content": content if isinstance(content, str) else str(content) + } + anthropic_messages.append({ + "role": "user", + "content": [tool_result_block] + }) + + return extracted_system, anthropic_messages + + +def _count_tokens_anthropic( + model: str, + messages: List[Dict], + system_prompt: Optional[str] = None, + tools: Optional[List[Dict]] = None +) -> Optional[int]: + """ + Count tokens using Anthropic's official API. + + Returns None if counting fails (caller should fall back to litellm). + """ + client = _get_anthropic_client() + if client is None: + return None + + try: + anthropic_model = _normalize_model_name(model, LLMProvider.ANTHROPIC) + system, anthropic_messages = _convert_messages_for_anthropic(messages, system_prompt) + + if not anthropic_messages: + return 0 + + kwargs = { + "model": anthropic_model, + "messages": anthropic_messages, + } + + if system: + kwargs["system"] = system + + if tools: + anthropic_tools = [] + for tool in tools: + if "function" in tool: + anthropic_tools.append({ + "name": tool["function"].get("name", ""), + "description": tool["function"].get("description", ""), + "input_schema": tool["function"].get("parameters", {}) + }) + else: + anthropic_tools.append(tool) + kwargs["tools"] = anthropic_tools + + response = client.messages.count_tokens(**kwargs) + + logger.debug("anthropic_token_count_success", extra={ + "model": anthropic_model, + "input_tokens": response.input_tokens, + "message_count": len(anthropic_messages) + }) + + return response.input_tokens + + except Exception as e: + logger.warning("anthropic_token_count_failed", extra={ + "model": model, + "error": str(e), + "fallback": "litellm" + }) + return None + + +# ============================================================================ +# OpenAI Token Counter (uses litellm - tiktoken is accurate for OpenAI) +# ============================================================================ + +def _count_tokens_openai( + model: str, + messages: List[Dict], + system_prompt: Optional[str] = None, + tools: Optional[List[Dict]] = None +) -> Optional[int]: + """ + Count tokens for OpenAI models. + + litellm uses tiktoken which IS accurate for OpenAI models, + so we just delegate directly. + """ + # litellm's tiktoken is accurate for OpenAI - no need for special handling + return None # Signal to use litellm fallback + + +# ============================================================================ +# Google Token Counter (placeholder for future implementation) +# ============================================================================ + +def _count_tokens_google( + model: str, + messages: List[Dict], + system_prompt: Optional[str] = None, + tools: Optional[List[Dict]] = None +) -> Optional[int]: + """ + Count tokens for Google/Gemini models. + + Google provides countTokens API but requires different message format. + For now, falls back to litellm. Can be implemented when needed. + + Reference: https://ai.google.dev/gemini-api/docs/tokens + """ + # TODO: Implement Google's countTokens API when needed + # from google import genai + # client.models.count_tokens(model=model, contents=messages) + return None # Signal to use litellm fallback + + +# ============================================================================ +# Provider Registry +# ============================================================================ + +# Map providers to their token counting functions +# Each function returns Optional[int] - None means "use litellm fallback" +PROVIDER_TOKEN_COUNTERS: Dict[LLMProvider, Callable] = { + LLMProvider.ANTHROPIC: _count_tokens_anthropic, + LLMProvider.OPENAI: _count_tokens_openai, + LLMProvider.GOOGLE: _count_tokens_google, +} + + +# ============================================================================ +# Litellm Fallback +# ============================================================================ + +def _count_tokens_litellm(model: str, messages: List[Dict]) -> int: + """ + Count tokens using litellm's token_counter. + + This is the universal fallback for all providers. + """ + try: + return litellm.token_counter(model=model, messages=messages) + except Exception as e: + logger.warning("litellm_token_count_failed", extra={ + "model": model, + "error": str(e), + "return_value": 0 + }) + return 0 + + +# ============================================================================ +# Main API +# ============================================================================ + +def count_tokens( + model: str, + text: Optional[str] = None, + messages: Optional[List[Dict]] = None, + system_prompt: Optional[str] = None, + tools: Optional[List[Dict]] = None, + llm_config: Optional[Dict[str, Any]] = None +) -> int: + """ + Count tokens using the most accurate method available for the given model. + + Provider routing: + - Anthropic (Claude): Uses official count_tokens API (accurate) + - OpenAI (GPT): Uses litellm/tiktoken (accurate for OpenAI) + - Google (Gemini): Falls back to litellm (future: use countTokens API) + - Unknown: Falls back to litellm + + Args: + model: The model name (e.g., 'claude-sonnet-4-20250514', 'gpt-4') + Typically comes from llm_config["model"] + text: Optional text string to count (converted to user message) + messages: Optional list of message dicts + system_prompt: Optional system prompt + tools: Optional list of tool definitions + llm_config: Optional LLM config dict from LLMConfigResolver + May contain model override via litellm_token_counter_model + + Returns: + Token count (0 on failure) + """ + # Determine the model to use for counting + model_for_counting = model + if llm_config: + # Check for explicit tokenizer model override + if llm_config.get("litellm_token_counter_model"): + model_for_counting = llm_config["litellm_token_counter_model"] + logger.debug("token_counting_model_override", extra={ + "original_model": model, + "override_model": model_for_counting + }) + # Or use the model from config if not provided + elif not model and llm_config.get("model"): + model_for_counting = llm_config["model"] + + if not model_for_counting: + logger.warning("token_count_no_model", extra={"return_value": 0}) + return 0 + + # Validate input + if text is not None and messages is not None: + raise ValueError("Provide either 'text' or 'messages', not both.") + + # Build messages list + messages_for_calc: List[Dict] = [] + + if system_prompt: + messages_for_calc.append({"role": "system", "content": system_prompt}) + + if text is not None: + messages_for_calc.append({"role": "user", "content": text}) + elif messages is not None: + messages_for_calc.extend(messages) + + if not messages_for_calc: + return 0 + + # Detect provider and route to appropriate counter + provider = _detect_provider(model_for_counting) + + if provider in PROVIDER_TOKEN_COUNTERS: + counter_fn = PROVIDER_TOKEN_COUNTERS[provider] + count = counter_fn( + model=model_for_counting, + messages=messages_for_calc, + system_prompt=system_prompt, + tools=tools + ) + + if count is not None: + return count + + # Provider counter returned None - fall back to litellm + logger.debug("provider_token_count_fallback", extra={ + "provider": provider.value, + "model": model_for_counting, + "fallback": "litellm" + }) + + # Use litellm for unknown providers or as fallback + return _count_tokens_litellm(model_for_counting, messages_for_calc) + + +# ============================================================================ +# Convenience exports for backward compatibility +# ============================================================================ + +def _is_anthropic_model(model: str) -> bool: + """Check if model is an Anthropic Claude model. Exported for testing.""" + return _detect_provider(model) == LLMProvider.ANTHROPIC + + +def _normalize_model_for_anthropic(model: str) -> str: + """Normalize model name for Anthropic API. Exported for testing.""" + return _normalize_model_name(model, LLMProvider.ANTHROPIC) diff --git a/core/agent_core/nodes/base_agent_node.py b/core/agent_core/nodes/base_agent_node.py index fab2101..f09f1b0 100644 --- a/core/agent_core/nodes/base_agent_node.py +++ b/core/agent_core/nodes/base_agent_node.py @@ -9,6 +9,14 @@ from pocketflow import AsyncNode from ..llm.call_llm import estimate_prompt_tokens, call_litellm_acompletion from ..framework.tool_registry import get_tool_by_name, get_tools_for_profile, format_tools_for_prompt_by_toolset +from ..framework.context_budget_guardian import ( + ContextBudgetGuardian, + ContextBudgetStatus, + assess_context_budget, + generate_context_budget_directive, + should_force_tool_call, + synthesize_partial_results +) import json_repair import os from typing import Dict, Any, Optional, List @@ -50,12 +58,13 @@ def __init__(self, super().__init__(max_retries=kwargs.pop('max_retries', 2), wait=kwargs.pop('wait', 3), **kwargs) self.profile_id = profile_id - self.agent_id = agent_id_override or profile_id + self.agent_id = agent_id_override or profile_id agent_id_var.set(self.agent_id) # Set context variable self.profile_instance_id_override = profile_instance_id_override self.parent_agent_id = parent_agent_id - + self.loaded_profile: Optional[Dict] = None + self.context_budget_guardian: Optional[ContextBudgetGuardian] = None # Initialized after LLM config is resolved if not shared_for_init: raise ValueError(f"AgentNode '{self.agent_id}': 'shared_for_init' (SubContext object) must be provided to __init__ for profile loading.") @@ -65,7 +74,7 @@ def __init__(self, shared_for_init["state"]["agent_start_utc_timestamp"] = datetime.now(timezone.utc).isoformat() shared_for_init["state"]["parent_agent_id"] = self.parent_agent_id shared_for_init["state"]["agent_id"] = self.agent_id - + if "meta" not in shared_for_init: shared_for_init["meta"] = {} shared_for_init["meta"]["agent_id"] = self.agent_id shared_for_init["meta"]["parent_agent_id"] = self.parent_agent_id @@ -84,7 +93,7 @@ def _load_profile(self, context: Dict): Raises ValueError if the profile (including fallback) is not found. """ agent_profiles_store = context['refs']['run']['config'].get("agent_profiles_store", {}) - + loaded_successfully = False if self.profile_instance_id_override: logger.debug("profile_load_by_instance_id_attempt", extra={"agent_id": self.agent_id, "profile_instance_id_override": self.profile_instance_id_override}) @@ -114,7 +123,7 @@ def _load_profile(self, context: Dict): available_profiles_summary.append(f" - Name: {prof_data.get('name', 'N/A')}, InstanceID: {inst_id}, Active: {prof_data.get('is_active')}, Deleted: {prof_data.get('is_deleted')}, Rev: {prof_data.get('rev')}") logger.error("profile_load_critical_failure", extra={"agent_id": self.agent_id, "profile_instance_id_override": self.profile_instance_id_override, "profile_id": self.profile_id, "fallback_logical_name": fallback_logical_name, "available_profiles": available_profiles_summary}, exc_info=True) raise ValueError(f"AgentProfile not found for agent '{self.agent_id}' (tried instance_id '{self.profile_instance_id_override}', name '{self.profile_id}', fallback '{fallback_logical_name}').") - + if self.profile_instance_id_override and self.loaded_profile and self.loaded_profile.get('name') != self.profile_id: original_profile_id_param = self.profile_id loaded_profile_actual_name = self.loaded_profile.get('name') @@ -131,17 +140,17 @@ def _update_assistant_message_in_state(self, state: Dict, llm_response: Dict): if msg.get("id") == placeholder_message_id: message_to_update = msg break - + if message_to_update: message_to_update["content"] = llm_response.get("content") or "" if llm_response.get("reasoning"): message_to_update["reasoning_content"] = llm_response.get("reasoning") if llm_response.get("tool_calls"): message_to_update["tool_calls"] = llm_response.get("tool_calls") - + message_to_update["timestamp"] = datetime.now(timezone.utc).isoformat() message_to_update['turn_id'] = state.get("current_turn_id") - + logger.debug("placeholder_message_updated", extra={"agent_id": self.agent_id, "placeholder_message_id": placeholder_message_id}) else: logger.warning("placeholder_message_not_found", extra={"agent_id": self.agent_id, "placeholder_message_id": placeholder_message_id}) @@ -168,7 +177,7 @@ async def _process_observers(self, observer_type: str, context: Dict): observer_id = config.get("id", "unnamed_observer") try: condition_str = config.get("condition", "True") - + # Evaluate condition should_run = False if condition_str == "True": @@ -195,18 +204,18 @@ async def _process_observers(self, observer_type: str, context: Dict): if action_type == "add_to_inbox": target_agent_id = action_config.get("target_agent_id", "self") inbox_item_template = action_config.get("inbox_item", {}) - + # Basic validation if not inbox_item_template.get("source"): raise ValueError("Observer action 'add_to_inbox' requires 'inbox_item.source'") raw_payload = inbox_item_template.get("payload", {}) resolved_payload = raw_payload - + if isinstance(raw_payload, str) and raw_payload.strip().startswith('{{') and raw_payload.strip().endswith('}}'): path_to_resolve = raw_payload.strip('{} ') actual_data = get_nested_value_from_context(context, path_to_resolve) - + if actual_data is not None: resolved_payload = actual_data else: @@ -223,7 +232,7 @@ async def _process_observers(self, observer_type: str, context: Dict): "triggering_observer_id": observer_id, } } - + # This is a simplified version. A real implementation would need to handle target_agent_id properly. # For now, we assume "self". state.setdefault("inbox", []).append(new_item) @@ -266,7 +275,7 @@ async def _construct_system_prompt(self, context: Dict) -> Dict[str, Any]: prompt_config = self.loaded_profile.get("system_prompt_construction", {}) segments = prompt_config.get("system_prompt_segments", []) text_definitions = self.loaded_profile.get("text_definitions", {}) - + prompt_parts = [] construction_log = [] @@ -300,17 +309,17 @@ async def _construct_system_prompt(self, context: Dict) -> Dict[str, Any]: try: if segment_type == "static_text": rendered_content = text_definitions.get(segment.get("content_key"), segment.get("content", "")) - + elif segment_type == "state_value": source_path = segment.get("source_state_path") ingestor_id = segment.get("ingestor_id") - + if not source_path: logger.warning("system_prompt_missing_source_path", extra={"segment_id": segment_id, "type": "state_value"}) rendered_content = "" else: raw_value = get_nested_value_from_context(context, source_path) - + if ingestor_id and ingestor_id in INGESTOR_REGISTRY: ingestor_func = INGESTOR_REGISTRY[ingestor_id] ingestor_params = segment.get("ingestor_params", {}) @@ -333,7 +342,7 @@ async def _construct_system_prompt(self, context: Dict) -> Dict[str, Any]: if isinstance(rendered_content, str): rendered_content = _apply_simple_template_interpolation(rendered_content, context) - + except Exception as e: logger.error("system_prompt_segment_error", extra={"segment_id": segment_id, "segment_type": segment_type, "error_message": str(e)}, exc_info=True) # Inject an error message into the prompt if a segment fails. @@ -350,7 +359,7 @@ async def _construct_system_prompt(self, context: Dict) -> Dict[str, Any]: ) prompt_parts.append(rendered_content) - + construction_log.append({ "segment_id": segment_id, "order": segment.get("order", 99), @@ -360,14 +369,14 @@ async def _construct_system_prompt(self, context: Dict) -> Dict[str, Any]: }) final_prompt = "\n\n".join(filter(None, prompt_parts)) - + return { "final_prompt": final_prompt, "construction_log": construction_log, } def _process_tool_calls(self, llm_response: Dict, context: Dict): - state = context["state"] + state = context["state"] turn_manager = context['refs']['run']['runtime'].get('turn_manager') tool_calls = llm_response.get("tool_calls") @@ -377,13 +386,13 @@ def _process_tool_calls(self, llm_response: Dict, context: Dict): tool_name = tool_call_to_process.get("function", {}).get("name") tool_arguments_str = tool_call_to_process.get("function", {}).get("arguments", "{}") tool_call_id = tool_call_to_process.get("id") - + # Step 1: Validate if the tool exists tool_info = get_tool_by_name(tool_name) if not tool_info: error_msg = f"LLM called an unregistered tool: '{tool_name}'." logger.error("tool_not_registered", extra={"agent_id": self.agent_id, "tool_name": tool_name, "tool_call_id": tool_call_id}, exc_info=True) - + # Step 1a: Record this failed attempt in the Turn if turn_manager: turn_manager.record_failed_tool_interaction(context, tool_call_to_process, error_msg) @@ -398,7 +407,7 @@ def _process_tool_calls(self, llm_response: Dict, context: Dict): }) state["current_action"] = None # Clear action return # Return early - + # Normal flow: Only proceed to parse arguments and create a 'running' interaction if the tool is valid. try: arguments = json_repair.loads(tool_arguments_str) @@ -407,7 +416,7 @@ def _process_tool_calls(self, llm_response: Dict, context: Dict): except Exception as e: error_msg = f"LLM provided invalid JSON arguments for tool '{tool_name}': {e}. Arguments string: '{tool_arguments_str}'" logger.error("tool_arguments_invalid", extra={"agent_id": self.agent_id, "tool_name": tool_name, "tool_call_id": tool_call_id, "arguments_string": tool_arguments_str, "error_message": str(e)}, exc_info=True) - + if turn_manager: turn_manager.record_failed_tool_interaction(context, tool_call_to_process, error_msg) @@ -449,7 +458,7 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: (V2) Determines the next action based on a list of rules in the profile's 'flow_decider'. """ state = context["state"] - + # Fallback to old mechanism if flow_decider is not defined if "flow_decider" not in self.loaded_profile: logger.warning("flow_decider_not_found", extra={"agent_id": self.agent_id}) @@ -459,7 +468,7 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: for rule in rules: rule_id = rule.get("id", "unnamed_rule") condition_str = rule.get("condition", "False") - + try: eval_globals = { "v": VModelAccessor(context), @@ -473,8 +482,14 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: action_type = action_config["type"] if action_type == "continue_with_tool": - return state.get("current_action", {}).get("tool_name") - + current_action = state.get("current_action", {}) + # Handle both dict and string formats (defensive) + if isinstance(current_action, dict): + return current_action.get("tool_name") + elif isinstance(current_action, str): + return current_action + return None + elif action_type == "end_agent_turn": # This action signals the flow should end. # We can store the outcome in the state for the finalizer. @@ -490,7 +505,7 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: if not payload.get("content_key"): logger.error("flow_decider_missing_content_key", extra={"rule_id": rule_id}, exc_info=True) continue - + state.setdefault("inbox", []).append({ "item_id": f"inbox_{rule_id}_{uuid.uuid4().hex[:4]}", "source": "SELF_REFLECTION_PROMPT", @@ -499,13 +514,13 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: "metadata": {"created_at": datetime.now(timezone.utc).isoformat(), "triggering_rule_id": rule_id} }) return "default" - + elif action_type == "await_user_input": return "await_user_input" else: logger.error("flow_decider_unknown_action", extra={"agent_id": self.agent_id, "action_type": action_type, "rule_id": rule_id}, exc_info=True) - + except Exception as e: logger.error("flow_decider_condition_error", extra={"agent_id": self.agent_id, "rule_id": rule_id, "error_message": str(e)}, exc_info=True) @@ -515,16 +530,21 @@ def _decide_next_action_with_flow_decider(self, context: Dict) -> str: def _determine_next_action_fallback(self, context: Dict) -> str: # This is the old logic, kept for compatibility. state = context["state"] - if state.get("current_action"): - return state["current_action"]["tool_name"] - + current_action = state.get("current_action") + if current_action: + # Handle both dict and string formats (defensive) + if isinstance(current_action, dict): + return current_action.get("tool_name") + elif isinstance(current_action, str): + return current_action + output_handler_config = self.loaded_profile.get("output_handling_config", {}).get("behavior_parameters_for_default_handler", {}) action_on_no_tool_call = output_handler_config.get("action_on_no_tool_call", "default") if action_on_no_tool_call != "default": logger.debug("no_tool_call_profile_action", extra={"agent_id": self.agent_id, "action_on_no_tool_call": action_on_no_tool_call}) return action_on_no_tool_call - + logger.debug("no_tool_call_default_loop", extra={"agent_id": self.agent_id}) return "default" @@ -547,7 +567,7 @@ def _resolve_dangling_tool_calls(self, context: Dict): last_assistant_message = messages[i] last_assistant_message_index = i break - + if not last_assistant_message or not last_assistant_message.get("tool_calls"): return @@ -559,7 +579,7 @@ def _resolve_dangling_tool_calls(self, context: Dict): if msg.get("role") == "assistant": break if msg.get("role") == "tool" and "tool_call_id" in msg: responded_tool_call_ids.add(msg["tool_call_id"]) - + for item in inbox: if item.get("source") == "TOOL_RESULT": payload = item.get("payload", {}) @@ -583,7 +603,7 @@ def _resolve_dangling_tool_calls(self, context: Dict): "tool_call_id": tool_call_id, "is_error": True, "content": { - "error": "tool_call_failed", + "error": "tool_call_failed", "message": "The tool did not produce a response, or its execution was interrupted before a result could be processed. Or, if you haved called more than one tool, the tool call was dropped as this agent only supports one tool call per turn.", } } @@ -593,7 +613,7 @@ def _resolve_dangling_tool_calls(self, context: Dict): "payload": tool_result_payload, "consumption_policy": "consume_on_read", "metadata": { - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(timezone.utc).isoformat(), "resolver": "dangling_call_resolver_v2" } }) @@ -608,7 +628,7 @@ async def prep_async(self, context: Dict) -> Dict: context["loaded_profile"] = self.loaded_profile await self._process_observers('pre_turn', context) - + self._resolve_dangling_tool_calls(context) # --- START: Refactored Inbox Processing --- @@ -618,16 +638,16 @@ async def prep_async(self, context: Dict) -> Dict: messages_for_llm = processing_result["messages_for_llm"] stream_id = f"stream_{self.agent_id}_{uuid.uuid4().hex[:8]}" - + turn_id = turn_manager.start_new_turn(context, stream_id) turn_id_var.set(turn_id) - + system_prompt_details = await self._construct_system_prompt(context) system_prompt = system_prompt_details["final_prompt"] - + hydrated_messages = await self._hydrate_messages(messages_for_llm) logger.debug("messages_hydrated", extra={"agent_id": self.agent_id, "before_count": len(messages_for_llm), "after_count": len(hydrated_messages)}) - + cleaned_messages = self._clean_messages_for_llm(hydrated_messages) logger.debug("messages_cleaned", extra={"agent_id": self.agent_id, "message_count": len(cleaned_messages)}) @@ -639,34 +659,85 @@ async def prep_async(self, context: Dict) -> Dict: # =============================================================== from ..llm.config_resolver import LLMConfigResolver - + resolver = LLMConfigResolver(shared_llm_configs=context['refs']['run']['config'].get("shared_llm_configs_ref", {})) final_llm_config = resolver.resolve(self.loaded_profile) predicted_total_tokens = estimate_prompt_tokens( model=final_llm_config.get("model"), - messages=final_messages_for_llm, # <-- Use the sanitized messages + messages=final_messages_for_llm, # <-- Use the sanitized messages system_prompt=system_prompt, llm_config_for_tokenizer=final_llm_config ) - + + # ==================== CONTEXT BUDGET GUARDIAN ==================== + # Initialize guardian on first turn, then track consumption + model_name = final_llm_config.get("model", "unknown") + if self.context_budget_guardian is None: + agent_type = self.loaded_profile.get("type") # e.g., "principal", "partner", "associate" + self.context_budget_guardian = ContextBudgetGuardian( + model_name=model_name, + llm_config=final_llm_config, + agent_id=self.agent_id, + agent_type=agent_type + ) + + budget_status, budget_metadata = self.context_budget_guardian.record_turn(predicted_total_tokens) + + # Get directive if budget is constrained + budget_directive = self.context_budget_guardian.get_directive(budget_status, budget_metadata) + + # Inject budget directive into system prompt if needed + if budget_directive: + system_prompt = f"{system_prompt}\n\n{budget_directive}" + logger.info("context_budget_directive_injected", extra={ + "agent_id": self.agent_id, + "status": budget_status.name, + "utilization_percent": budget_metadata["utilization_percent"] + }) + + # Store budget status in context for potential use by flow_decider or tools + context["state"]["_context_budget"] = { + "status": budget_status.name, + "utilization_percent": budget_metadata["utilization_percent"], + "remaining_tokens": budget_metadata["remaining_tokens"], + "force_completion": budget_status in (ContextBudgetStatus.CRITICAL, ContextBudgetStatus.EXCEEDED) + } + + # CIRCUIT BREAKER: If EXCEEDED, skip LLM call entirely + skip_llm_call = budget_status == ContextBudgetStatus.EXCEEDED + if skip_llm_call: + logger.warning( + "context_budget_circuit_breaker_triggered", + extra={ + "agent_id": self.agent_id, + "status": budget_status.name, + "utilization_percent": budget_metadata["utilization_percent"], + "action": "skipping_llm_call_forcing_summarization" + } + ) + # =============================================================== + api_tools_list = get_formatted_api_tools(self, context) self.max_retries = final_llm_config.get("max_retries", self.max_retries) self.wait = final_llm_config.get("wait_seconds_on_retry", self.wait) - + llm_call_package = { - "messages_for_llm": final_messages_for_llm, # <-- Use the sanitized messages + "messages_for_llm": final_messages_for_llm, # <-- Use the sanitized messages "system_prompt_content": system_prompt, "final_llm_config": final_llm_config, "api_tools_list": api_tools_list, "stream_id": stream_id, "context_for_exec": context, - "predicted_total_tokens": predicted_total_tokens + "predicted_total_tokens": predicted_total_tokens, + "context_budget_status": budget_status.name, # Include for downstream use + "skip_llm_call": skip_llm_call, # Circuit breaker flag + "agent_type": agent_type # For circuit breaker tool selection } - + turn_manager.enrich_turn_inputs(context, turn_id, processing_result, llm_call_package, system_prompt_details) - + return llm_call_package except Exception as e: error_msg = f"Unhandled exception in prep_async: {e}" @@ -674,7 +745,7 @@ async def prep_async(self, context: Dict) -> Dict: if turn_manager: turn_manager.fail_current_turn(context, error_msg) raise - + async def exec_async(self, prep_res: Dict) -> Dict: """ (Modified) Calls the LLM and returns the aggregated result, or a standard error dictionary on failure. @@ -686,6 +757,80 @@ async def exec_async(self, prep_res: Dict) -> Dict: run_id = context['meta'].get("run_id") initial_params = flow_specific_state.get("initial_parameters", {}) + # =============================================================== + # CIRCUIT BREAKER: Skip LLM call if context budget exceeded + # =============================================================== + if prep_res.get("skip_llm_call"): + logger.warning( + "exec_async_skipped_due_to_context_budget", + extra={"agent_id": self.agent_id, "run_id": run_id} + ) + + # Synthesize partial results from completed work + team_state = context.get('refs', {}).get('run', {}).get('team_state', {}) + budget_metadata = context.get('state', {}).get('_context_budget', {}) + + synthesis = synthesize_partial_results( + team_state=team_state, + triggered_agent_id=self.agent_id, + budget_metadata=budget_metadata + ) + + # Return a synthetic response that forces flow completion + # Tool selection depends on agent type: + # - Principal/Partner: use finish_flow to wrap up the session + # - Associates: use generate_message_summary to submit deliverables + forced_tool_call_id = f"forced_circuit_breaker_{uuid.uuid4().hex[:8]}" + agent_type = prep_res.get("agent_type") or "associate" # From profile's "type" field + + if agent_type in ("principal", "partner"): + # Principal/Partner should gracefully end the flow with synthesis + forced_tool_name = "finish_flow" + forced_tool_args = { + "reason": f"Context budget exceeded ({budget_metadata.get('utilization_percent', '>70')}% utilization). Circuit breaker triggered.", + "partial_results_synthesis": synthesis.get("user_message", "Partial results not available."), + "completed_modules": synthesis.get("summary", {}).get("completed", 0), + "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) + } + else: + # Associates should submit their current findings + forced_tool_name = "generate_message_summary" + forced_tool_args = { + "reason": f"Context budget exceeded ({budget_metadata.get('utilization_percent', '>70')}% utilization). Circuit breaker triggered.", + "partial_work_summary": synthesis.get("user_message", "Work interrupted due to context limits.") + } + + logger.info( + "circuit_breaker_tool_selected", + extra={ + "agent_id": self.agent_id, + "agent_type": agent_type, + "tool_name": forced_tool_name, + "completed_modules": synthesis.get("summary", {}).get("completed", 0), + "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) + } + ) + + # Include the synthesis in the response content for user visibility + synthesis_content = synthesis.get("user_message", "") + + return { + "content": f"[CONTEXT BUDGET EXCEEDED - Automatic {forced_tool_name} triggered]\n\n{synthesis_content}", + "tool_calls": [{ + "id": forced_tool_call_id, + "type": "function", + "function": { + "name": forced_tool_name, + "arguments": json.dumps(forced_tool_args) + } + }], + "reasoning": None, + "model_id_used": "circuit_breaker", + "error": None, + "circuit_breaker_synthesis": synthesis # Include full synthesis for downstream processing + } + # =============================================================== + # Create a placeholder message ID placeholder_message_id = f"msg_{prep_res['stream_id']}" placeholder_message = { @@ -722,9 +867,9 @@ async def exec_async(self, prep_res: Dict) -> Dict: contextual_data_for_event=contextual_data_for_event, run_context=context['refs']['run'] ) - + aggregated_llm_output['placeholder_message_id'] = placeholder_message_id - + turn_manager = context['refs']['run']['runtime'].get('turn_manager') if turn_manager: turn_manager.update_llm_interaction_end(context, aggregated_llm_output) @@ -755,13 +900,13 @@ async def exec_async(self, prep_res: Dict) -> Dict: "error": error_msg, "error_type": type(e).__name__, "placeholder_message_id": placeholder_message_id, - "actual_usage": None, - "content": None, - "tool_calls": [], - "reasoning": None, + "actual_usage": None, + "content": None, + "tool_calls": [], + "reasoning": None, "model_id_used": None } - + async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str: logger.debug("post_async_started", extra={"agent_id": self.agent_id}) state = context["state"] @@ -778,7 +923,7 @@ async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str if turn_manager: turn_manager.fail_current_turn(context, error_message) - + self._update_assistant_message_in_state(state, llm_response) if events_for_post: @@ -787,10 +932,10 @@ async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str agent_id=self.agent_id, error_message=f"Agent '{self.agent_id}' encountered a critical error: {error_message}" ) - + next_action = "error" return next_action - + if turn_manager: turn_manager.update_llm_interaction_end(context, llm_response) @@ -798,13 +943,66 @@ async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str if isinstance(llm_response.get("tool_calls"), list) and len(llm_response["tool_calls"]) > 1: logger.warning("multiple_tool_calls_detected", extra={"agent_id": self.agent_id, "total_calls": len(llm_response['tool_calls']), "dropped_calls": llm_response['tool_calls'][1:]}) llm_response["tool_calls"] = llm_response["tool_calls"][:1] # Keep only the first call + + # =============================================================== + # CONTEXT BUDGET GUARDIAN: Force tool call at EXCEEDED threshold + # =============================================================== + context_budget = state.get("_context_budget", {}) + budget_status_name = context_budget.get("status") + if budget_status_name: + try: + budget_status = ContextBudgetStatus[budget_status_name] + # Get agent_type from profile to select correct forced tool + agent_type = self.loaded_profile.get("type") if self.loaded_profile else None + forced_tool = should_force_tool_call(budget_status, agent_type=agent_type) + + if forced_tool: + # Check if LLM already called the required tool + current_tool_calls = llm_response.get("tool_calls", []) + already_calling_forced = any( + tc.get("function", {}).get("name") == forced_tool + for tc in current_tool_calls + ) + + if not already_calling_forced: + logger.warning( + "context_budget_forcing_tool_call", + extra={ + "agent_id": self.agent_id, + "status": budget_status_name, + "forced_tool": forced_tool, + "utilization_percent": context_budget.get("utilization_percent") + } + ) + # Inject the forced tool call + forced_tool_call = { + "id": f"forced_tc_{uuid.uuid4().hex[:8]}", + "type": "function", + "function": { + "name": forced_tool, + "arguments": json.dumps({"reason": "Context budget exceeded - automatic summarization triggered"}) + } + } + llm_response["tool_calls"] = [forced_tool_call] + # current_action must be a dict, not a string + state["current_action"] = { + "tool_name": forced_tool, + "tool_call_id": forced_tool_call["id"], + "parameters": {"reason": "Context budget exceeded - automatic summarization triggered"} + } + state["current_tool_call_id"] = forced_tool_call["id"] + state["current_tool_arguments"] = {"reason": "Context budget exceeded - automatic summarization triggered"} + except (KeyError, ValueError) as e: + logger.debug("context_budget_status_parse_error", extra={"error": str(e)}) + # =============================================================== + self._update_assistant_message_in_state(state, llm_response) # 2. Execute Post-Turn Observers await self._process_observers('post_turn', context) - + # 3. Decide the next action based on the Profile next_action = self._decide_next_action_with_flow_decider(context) - + logger.info("turn_completed", extra={"agent_id": self.agent_id, "next_action": next_action}) return next_action except Exception as e: @@ -831,21 +1029,21 @@ async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str run_id=run_id_for_post, turn_id=current_turn_id, agent_id=self.agent_id - ) + ) await trigger_view_model_update(context, "flow_view") await trigger_view_model_update(context, "timeline_view") await trigger_view_model_update(context, "kanban_view") await events_for_post.emit_turns_sync(context) - + def _extract_purpose_from_tool_result(self, payload: Dict, context: Dict) -> str: """Extract purpose/context from tool result to create unique source_uri.""" tool_name = payload.get("tool_name", "unknown") tool_content = payload.get("content", {}) - + # Try to get purpose from current action context current_action = context.get("state", {}).get("current_action", {}) agent_profile = self.loaded_profile.get("name", "unknown") - + # Generate purpose based on tool type and context if tool_name in ["jina_search", "web_search"]: if isinstance(tool_content, dict): @@ -853,7 +1051,7 @@ def _extract_purpose_from_tool_result(self, payload: Dict, context: Dict) -> str purpose = f"search_{hash(query) % 10000}" # Hash to keep it short else: purpose = "search_results" - + elif tool_name == "jina_visit": if isinstance(tool_content, dict): url = tool_content.get("url", "") @@ -870,7 +1068,7 @@ def _extract_purpose_from_tool_result(self, payload: Dict, context: Dict) -> str purpose = "page_content" else: purpose = "page_content" - + elif tool_name == "dispatch_submodules": # For dispatcher, include some context about the assignment if isinstance(tool_content, dict): @@ -883,16 +1081,16 @@ def _extract_purpose_from_tool_result(self, payload: Dict, context: Dict) -> str purpose = "dispatch_general" else: purpose = "dispatch_result" - + elif tool_name == "generate_markdown_report": purpose = "final_report" - + else: # For other tools, use agent profile and tool name purpose = f"{agent_profile}_{tool_name}".replace("_", "")[:20] - + return purpose - + async def _hydrate_messages(self, dehydrated_messages: List[Dict]) -> List[Dict]: """ [Refactored] Simplified message hydration logic, fully delegated to the Knowledge Base (KB). @@ -912,46 +1110,76 @@ async def _hydrate_messages(self, dehydrated_messages: List[Dict]) -> List[Dict] logger.error("message_hydration_failed", extra={"agent_id": self.agent_id, "error_message": str(e)}, exc_info=True) # Keep the original (dehydrated) content as a fallback hydrated_msg['content'] = msg.get('content') - + hydrated_messages.append(hydrated_msg) - + logger.debug("message_hydration_complete", extra={"message_count": len(hydrated_messages)}) return hydrated_messages - + def _clean_messages_for_llm(self, messages: List[Dict]) -> List[Dict]: """Cleans messages, removes internal fields, and ensures all content is LLM-processable text.""" + import json cleaned_messages = [] - + for msg in messages: # Create a clean copy of the message cleaned_msg = {} - + # Keep only the standard fields required by the LLM for key in ["role", "content", "tool_calls", "tool_call_id", "name"]: if key in msg: value = msg[key] - + # Ensure content is a string if key == "content": if isinstance(value, dict): # If content is a dictionary, convert it to a JSON string - import json cleaned_msg[key] = json.dumps(value, ensure_ascii=False) logger.debug("dict_content_converted_to_json", extra={"message_role": msg.get('role')}) elif value is None: cleaned_msg[key] = "" # Prevent None content else: cleaned_msg[key] = str(value) # Ensure it is a string + elif key == "tool_calls": + # Sanitize tool_calls to ensure arguments are valid JSON objects + # Anthropic requires tool_use.input to be a dictionary, not a string + sanitized_tool_calls = [] + for tc in value: + tc_copy = dict(tc) # Shallow copy + if "function" in tc_copy: + func = dict(tc_copy["function"]) # Copy function dict + args_str = func.get("arguments", "{}") + # Ensure arguments parse to a dict, default to {} if malformed + try: + parsed_args = json.loads(args_str) if isinstance(args_str, str) else args_str + if not isinstance(parsed_args, dict): + logger.warning("tool_call_arguments_sanitized", extra={ + "tool_name": func.get("name"), + "original_args": args_str, + "reason": "not_a_dict" + }) + parsed_args = {} + except (json.JSONDecodeError, TypeError): + logger.warning("tool_call_arguments_sanitized", extra={ + "tool_name": func.get("name"), + "original_args": args_str, + "reason": "json_decode_error" + }) + parsed_args = {} + func["arguments"] = json.dumps(parsed_args) + tc_copy["function"] = func + sanitized_tool_calls.append(tc_copy) + cleaned_msg[key] = sanitized_tool_calls else: cleaned_msg[key] = value - + # Filter out internal fields (those starting with _) internal_fields = [k for k in msg.keys() if k.startswith('_')] if internal_fields: logger.debug("internal_fields_removed", extra={"internal_fields": internal_fields}) - + cleaned_messages.append(cleaned_msg) - + return cleaned_messages def _finalize_dangling_tool_in_turn(self, context: Dict): @@ -961,7 +1189,7 @@ def _finalize_dangling_tool_in_turn(self, context: Dict): """ state = context.get("state", {}) team_state = context.get("refs", {}).get("team", {}) - + current_turn_id = state.get("current_turn_id") current_tool_call_id = state.get("current_tool_call_id") @@ -971,11 +1199,11 @@ def _finalize_dangling_tool_in_turn(self, context: Dict): # Find the current Turn current_turn = next((t for t in reversed(team_state.get("turns", [])) if t.get("turn_id") == current_turn_id), None) - + if current_turn: # Find the corresponding tool_interaction in this turn that is still 'running' tool_interaction_to_update = next(( - ti for ti in current_turn.get("tool_interactions", []) + ti for ti in current_turn.get("tool_interactions", []) if ti.get("tool_call_id") == current_tool_call_id and ti.get("status") == "running" ), None) diff --git a/core/agent_core/nodes/custom_nodes/dispatcher_node.py b/core/agent_core/nodes/custom_nodes/dispatcher_node.py index d79aed7..292664e 100644 --- a/core/agent_core/nodes/custom_nodes/dispatcher_node.py +++ b/core/agent_core/nodes/custom_nodes/dispatcher_node.py @@ -3,18 +3,26 @@ import logging import asyncio import copy -import uuid +import uuid from datetime import datetime, timezone # Added timezone -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional, Tuple from pocketflow import AsyncParallelBatchNode # Ensure this is the correct base class from ...framework.tool_registry import tool_registry # from nodes.base_agent_node import AgentNode # Not directly used here for instantiation -from ...state.management import _create_flow_specific_state_template +from ...state.management import _create_flow_specific_state_template from ...framework.profile_utils import get_active_profile_by_name from ...framework.handover_service import HandoverService -# +++ START: New imports +++ -# from utils.server_manager import initialize_mcp_session_for_context # No longer needed -# +++ END: New imports +++ +# Content selection for budget-aware inheritance +from ...utils.content_selection import ( + compute_inheritance_budget_chars, + select_inherited_content_with_hydration, + format_inherited_content_for_briefing, + STRATEGY_LLM_SUMMARY, + STRATEGY_NEWEST_FIRST, + STRATEGY_EMPTY +) +from ...framework.context_budget_guardian import get_model_context_limit +from ...llm.config_resolver import LLMConfigResolver logger = logging.getLogger(__name__) @@ -27,7 +35,7 @@ @tool_registry( name="dispatch_submodules", # Associate the tool with our newly created protocol - handover_protocol="principal_to_associate_briefing", + handover_protocol="principal_to_associate_briefing", description=DESCRIPTION, parameters={ "type": "object", @@ -61,7 +69,7 @@ }, "required": ["assignments"] }, - default_knowledge_item_type="DISPATCH_SUBMODULES_RESULT" + default_knowledge_item_type="DISPATCH_SUBMODULES_RESULT" ) class DispatcherNode(AsyncParallelBatchNode): """ @@ -72,6 +80,157 @@ def __init__(self, **kwargs): super().__init__(**kwargs) logger.debug("dispatcher_node_initialized") + async def _preselect_inherited_content( + self, + inherit_messages_from: List[str], + work_modules: Dict[str, Any], + run_context: Dict, + target_profile_logical_name: str + ) -> Tuple[List[Dict], Dict[str, Any]]: + """ + Pre-select content from source modules within a computed budget. + + This method implements budget-aware content inheritance to prevent + new Associates from being "born over-budget". + + Algorithm: + 1. Resolve target agent's context limit from its LLM config + 2. Compute per-source budget: (limit * 0.40) / num_sources + 3. For each source, use two-tier selection: + - Tier 1: Use deliverables.primary_summary if it fits + - Tier 2: Fall back to newest-first message selection + 4. Hydrate messages BEFORE selection to get accurate sizing + + Args: + inherit_messages_from: List of source module IDs + work_modules: Dict of all work modules + run_context: Global run context (for KB and config access) + target_profile_logical_name: Profile name of the spawning agent + + Returns: + Tuple of (preselected_messages, selection_metadata) + """ + if not inherit_messages_from: + return [], {"skipped": True, "reason": "no_sources"} + + # Get Knowledge Base for hydration + kb = run_context.get("runtime", {}).get("knowledge_base") + + # Resolve target agent's context limit + agent_profiles_store = run_context.get("config", {}).get("agent_profiles_store", {}) + shared_llm_configs = run_context.get("config", {}).get("shared_llm_configs_ref", {}) + + target_context_limit = 200000 # Default conservative limit + + try: + target_profile = get_active_profile_by_name(agent_profiles_store, target_profile_logical_name) + if target_profile: + llm_config_ref = target_profile.get("llm_config_ref") + if llm_config_ref and shared_llm_configs: + resolver = LLMConfigResolver(shared_llm_configs) + resolved_config = resolver.resolve(target_profile) + model_name = resolved_config.get("model", "") + target_context_limit = get_model_context_limit(model_name, resolved_config) + except Exception as e: + logger.warning("dispatcher_context_limit_resolution_failed", extra={ + "profile": target_profile_logical_name, + "error": str(e), + "fallback": target_context_limit + }) + + # Compute per-source budget + num_sources = len(inherit_messages_from) + per_source_budget = compute_inheritance_budget_chars(target_context_limit, num_sources) + + logger.info("dispatcher_preselect_started", extra={ + "inherit_from": inherit_messages_from, + "target_context_limit": target_context_limit, + "per_source_budget_chars": per_source_budget + }) + + # Process each source module + all_preselected = [] + selection_metadata = { + "target_context_limit": target_context_limit, + "per_source_budget_chars": per_source_budget, + "sources": {} + } + + for source_module_id in inherit_messages_from: + source_module = work_modules.get(source_module_id) + if not source_module: + logger.warning("dispatcher_preselect_source_not_found", extra={ + "source_module_id": source_module_id + }) + selection_metadata["sources"][source_module_id] = { + "error": "module_not_found" + } + continue + + # Get the most recent context_archive entry + context_archive = source_module.get("context_archive", []) + if not context_archive: + logger.debug("dispatcher_preselect_no_archive", extra={ + "source_module_id": source_module_id + }) + selection_metadata["sources"][source_module_id] = { + "error": "no_context_archive" + } + continue + + latest_archive = context_archive[-1] + + # Perform selection with hydration + try: + selected_content, content_metadata = await select_inherited_content_with_hydration( + context_archive_entry=latest_archive, + budget_chars=per_source_budget, + knowledge_base=kb, + source_id=source_module_id + ) + + # Format for briefing injection + formatted_messages = format_inherited_content_for_briefing( + content=selected_content, + metadata=content_metadata, + source_id=source_module_id + ) + + all_preselected.extend(formatted_messages) + selection_metadata["sources"][source_module_id] = content_metadata + + logger.info("dispatcher_preselect_source_complete", extra={ + "source_module_id": source_module_id, + "strategy": content_metadata.get("strategy"), + "chars_used": content_metadata.get("chars_used", 0), + "items_selected": content_metadata.get("items_selected", 0) + }) + + except Exception as e: + logger.error("dispatcher_preselect_source_failed", extra={ + "source_module_id": source_module_id, + "error": str(e) + }, exc_info=True) + selection_metadata["sources"][source_module_id] = { + "error": str(e) + } + + total_chars = sum( + meta.get("chars_used", 0) + for meta in selection_metadata["sources"].values() + if isinstance(meta, dict) and "chars_used" in meta + ) + selection_metadata["total_chars_selected"] = total_chars + selection_metadata["total_messages_selected"] = len(all_preselected) + + logger.info("dispatcher_preselect_complete", extra={ + "total_chars": total_chars, + "total_messages": len(all_preselected), + "sources_processed": len(inherit_messages_from) + }) + + return all_preselected, selection_metadata + async def prep_async(self, shared: Dict) -> List[Dict]: logger.debug("dispatcher_prep_async_started") # shared is principal's SubContext @@ -99,6 +258,13 @@ async def prep_async(self, shared: Dict) -> List[Dict]: assigned_module_ids_in_this_call = set() for assign_idx, assignment_item in enumerate(assignments_input): + # Defensive: LLM may return malformed data (e.g., strings instead of dicts) + if not isinstance(assignment_item, dict): + err_msg = f"Assignment at index {assign_idx} is not a dict (got {type(assignment_item).__name__}). Raw value: {str(assignment_item)[:100]}" + logger.warning("dispatcher_prep_malformed_assignment", extra={"index": assign_idx, "type": type(assignment_item).__name__, "error_message": err_msg}) + failed_assignments_at_prep.append({"input": str(assignment_item)[:100], "reason": err_msg}) + continue + module_id = assignment_item.get("module_id_to_assign") agent_profile_logical_name = assignment_item.get("agent_profile_logical_name") assigned_role_name = assignment_item.get("assigned_role_name") @@ -128,7 +294,7 @@ async def prep_async(self, shared: Dict) -> List[Dict]: if not actual_profile_details: failed_assignments_at_prep.append({"input": assignment_item, "reason": f"Profile '{agent_profile_logical_name}' not found or inactive."}) continue - + assignment_package = { "original_assignment_input": assignment_item, "resolved_profile_instance_id": actual_profile_details.get("profile_id"), @@ -163,7 +329,7 @@ async def exec_async(self, assignment_package: Dict) -> Dict: module_id = module_to_execute["module_id"] executing_associate_id = assignment_package["executing_associate_id"] profile_logical_name = assignment_package["resolved_profile_logical_name"] - + logger.info("dispatcher_exec_assignment_started", extra={ "module_id": module_id, "profile_logical_name": profile_logical_name, @@ -173,7 +339,7 @@ async def exec_async(self, assignment_package: Dict) -> Dict: module_to_update = copy.deepcopy(team_state_global.get("work_modules", {}).get(module_id)) if not module_to_update: return {"error": f"Module {module_id} not found at execution time."} - + start_time_iso = datetime.now(timezone.utc).isoformat() module_to_update["status"] = "ongoing" module_to_update["updated_at"] = start_time_iso @@ -200,22 +366,57 @@ async def exec_async(self, assignment_package: Dict) -> Dict: team_state_global.setdefault("dispatch_history", []).append(history_entry) logger.info("dispatcher_history_entry_added", extra={"executing_associate_id": executing_associate_id, "status": "LAUNCHING"}) + # --- Budget-Aware Content Pre-Selection --- + # If inherit_messages_from is specified, pre-select content within budget + # BEFORE calling HandoverService to ensure budget compliance + original_assignment = assignment_package.get("original_assignment_input", {}) + inherit_messages_from = original_assignment.get("inherit_messages_from", []) + preselected_messages = [] + preselection_metadata = {} + + if inherit_messages_from: + try: + preselected_messages, preselection_metadata = await self._preselect_inherited_content( + inherit_messages_from=inherit_messages_from, + work_modules=team_state_global.get("work_modules", {}), + run_context=run_context_global, + target_profile_logical_name=profile_logical_name + ) + logger.info("dispatcher_content_preselection_complete", extra={ + "module_id": module_id, + "total_chars": preselection_metadata.get("total_chars_selected", 0), + "total_messages": len(preselected_messages) + }) + except Exception as e: + logger.error("dispatcher_content_preselection_failed", extra={ + "module_id": module_id, + "error": str(e) + }, exc_info=True) + # Continue with empty preselected content - let HandoverService use fallback + # --- End Budget-Aware Content Pre-Selection --- + try: # Build a temporary source_context to simulate the state when the Principal calls the tool + # Inject pre-selected content into parameters for HandoverService to use + enhanced_parameters = original_assignment.copy() + if preselected_messages: + enhanced_parameters["_preselected_inherited_messages"] = preselected_messages + enhanced_parameters["_preselection_metadata"] = preselection_metadata + temp_source_context_for_handover = { - "state": { + "state": { "current_action": { # Place the current assignment's parameters into current_action.parameters - "parameters": assignment_package.get("original_assignment_input", {}) + "parameters": enhanced_parameters } }, "refs": parent_context["refs"], "meta": parent_context["meta"] } - + # Call HandoverService inbox_item_data = await HandoverService.execute( - "principal_to_associate_briefing", + "principal_to_associate_briefing", temp_source_context_for_handover ) @@ -231,14 +432,14 @@ async def exec_async(self, assignment_package: Dict) -> Dict: "consumption_policy": "consume_on_read", "metadata": {"created_at": datetime.now(timezone.utc).isoformat()} }) - + principal_last_turn_id = parent_context['state'].get("last_turn_id") associate_sub_context_state['last_turn_id'] = principal_last_turn_id logger.debug("dispatcher_last_turn_id_passed", extra={"last_turn_id": principal_last_turn_id, "executing_associate_id": executing_associate_id}) principal_agent_id = parent_context['meta'].get("agent_id") assigned_role_name = assignment_package.get("assigned_role_name") - + associate_sub_context: Dict[str, Any] = { "meta": { "run_id": run_id, @@ -255,9 +456,9 @@ async def exec_async(self, assignment_package: Dict) -> Dict: "runtime_objects": {}, "refs": { "run": run_context_global, "team": team_state_global } } - + logger.info("dispatcher_associate_starting", extra={"executing_associate_id": executing_associate_id}) - + completed_associate_context = None associate_exec_status = "error" last_turn_id = None @@ -268,7 +469,7 @@ async def exec_async(self, assignment_package: Dict) -> Dict: logger.info("dispatcher_associate_task_registered", extra={"executing_associate_id": executing_associate_id}) from ...flow import run_associate_async completed_associate_context = await run_associate_async(associate_sub_context) - + final_associate_state = completed_associate_context.get("state", {}) last_turn_id = final_associate_state.get("last_turn_id") @@ -280,17 +481,17 @@ async def exec_async(self, assignment_package: Dict) -> Dict: final_associate_state = completed_associate_context.setdefault("state", {}) final_associate_state["error_message"] = f"Dispatcher critical error: {str(e)}" final_associate_state.setdefault("deliverables", {})["error"] = f"Dispatcher critical error: {str(e)}" - + finally: end_time_iso = datetime.now(timezone.utc).isoformat() final_outcome = "completed_success" if associate_exec_status == "success" else "completed_error" - + final_associate_state = completed_associate_context.get("state", {}) if completed_associate_context else {} deliverables_from_associate = final_associate_state.get("deliverables", {}) error_details_from_associate = final_associate_state.get("error_message") - + all_messages = final_associate_state.get("messages", []) - + # Filter out messages that are marked as not for handover (e.g., initial briefings). # The msg.get("_internal", {}) ensures safe access even if the _internal key doesn't exist. new_messages_from_associate = [ @@ -309,7 +510,7 @@ async def exec_async(self, assignment_package: Dict) -> Dict: summary = ", ".join(deliverables_from_associate.keys()) history_entry_to_update["final_summary"] = f"Deliverables: {summary}" logger.info("dispatcher_history_updated", extra={"executing_associate_id": executing_associate_id, "new_status": history_entry_to_update['status']}) - + history_list = module_to_update.get("assignee_history", []) entry_to_update = next((h for h in reversed(history_list) if h.get("dispatch_id") == executing_associate_id and h.get("outcome") == "running"), None) if entry_to_update: @@ -320,7 +521,7 @@ async def exec_async(self, assignment_package: Dict) -> Dict: "dispatch_id": executing_associate_id, "archived_at": end_time_iso, "messages": final_associate_state.get("messages", []), "deliverables": deliverables_from_associate }) - + module_to_update["status"] = "pending_review" module_to_update["review_info"] = { "trigger": "associate_completed" if associate_exec_status == "success" else "associate_failed", @@ -337,11 +538,11 @@ async def exec_async(self, assignment_package: Dict) -> Dict: logger.info("dispatcher_associate_task_deregistered", extra={"executing_associate_id": executing_associate_id}) return { - "executing_associate_id": executing_associate_id, + "executing_associate_id": executing_associate_id, "module_id": module_id, - "agent_profile_logical_name_used": profile_logical_name, + "agent_profile_logical_name_used": profile_logical_name, "status_of_associate_execution": associate_exec_status, - "deliverables_from_associate": deliverables_from_associate, + "deliverables_from_associate": deliverables_from_associate, "error_detail_from_associate": error_details_from_associate, "last_turn_id": last_turn_id, "new_messages_from_associate": new_messages_from_associate @@ -354,7 +555,7 @@ async def post_async(self, shared: Dict, prep_res: List[Dict], exec_res_list: Li logger.debug("dispatcher_post_async_aggregating", extra={"execution_count": len(exec_res_list), "dispatch_tool_call_id": dispatch_tool_call_id}) failed_assignments_from_prep = principal_state.pop("_temp_dispatcher_prep_failures", []) - + num_launched_modules = len(exec_res_list) num_successful_executions = sum(1 for res in exec_res_list if res.get("status_of_associate_execution") == "success") num_failed_executions = num_launched_modules - num_successful_executions @@ -373,7 +574,7 @@ async def post_async(self, shared: Dict, prep_res: List[Dict], exec_res_list: Li overall_dispatch_op_status = "TOTAL_FAILURE_ASSOCIATES_ALL_FAILED" if num_prep_failures == 0 else "TOTAL_FAILURE_PREP_AND_ASSOC_FAILED" elif num_prep_failures > 0 and num_launched_modules == 0 : overall_dispatch_op_status = "TOTAL_FAILURE_ALL_PREP_FAILED" - + dispatch_op_message = ( f"Dispatch operation concluded for {original_assignments_requested_count} requested assignment(s). " f"{num_launched_modules} module(s) were dispatched. " @@ -406,13 +607,13 @@ async def post_async(self, shared: Dict, prep_res: List[Dict], exec_res_list: Li team_state_from_refs_post = shared['refs']['team'] run_id_from_meta_post = shared['meta']['run_id'] turn_manager = shared['refs']['run']['runtime'].get('turn_manager') - + # Find the Turn that initiated this dispatch dispatch_turn = turn_manager._get_turn_by_id(team_state_from_refs_post, principal_state.get("current_turn_id")) if turn_manager else None if dispatch_turn and turn_manager: last_turn_ids_of_subflows = [res.get("last_turn_id") for res in exec_res_list if res.get("last_turn_id")] - + # Call TurnManager to create the aggregation turn aggregation_turn_id = turn_manager.create_aggregation_turn( team_state=team_state_from_refs_post, @@ -422,7 +623,7 @@ async def post_async(self, shared: Dict, prep_res: List[Dict], exec_res_list: Li dispatch_tool_call_id=dispatch_tool_call_id, aggregation_summary=f"{num_successful_executions}/{num_launched_modules} successful." ) - + # Pass the "baton" to the new aggregation turn principal_state['last_turn_id'] = aggregation_turn_id logger.debug("dispatcher_relay_baton_passed", extra={"aggregation_turn_id": aggregation_turn_id}) @@ -448,11 +649,11 @@ async def post_async(self, shared: Dict, prep_res: List[Dict], exec_res_list: Li "consumption_policy": "consume_on_read", "metadata": {"created_at": datetime.now(timezone.utc).isoformat()} }) - + logger.info("dispatcher_post_async_completed", extra={"overall_status": overall_dispatch_op_status}) - + principal_state["current_action"] = None - + try: from ...events.event_triggers import trigger_view_model_update @@ -471,7 +672,7 @@ async def run_batch_async(self, shared: Dict, prep_res_list: List[Dict]) -> List tasks = [] for prep_item in prep_res_list: tasks.append(self.exec_async(prep_item)) - + exec_res_list = await asyncio.gather(*tasks, return_exceptions=True) processed_exec_res_list = [] diff --git a/core/agent_core/nodes/custom_nodes/finish_node.py b/core/agent_core/nodes/custom_nodes/finish_node.py index 771da11..8a4ffe3 100644 --- a/core/agent_core/nodes/custom_nodes/finish_node.py +++ b/core/agent_core/nodes/custom_nodes/finish_node.py @@ -1,16 +1,154 @@ import logging +import re from ..base_tool_node import BaseToolNode from pocketflow import AsyncNode from ...framework.tool_registry import tool_registry -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List import uuid from datetime import datetime, timezone logger = logging.getLogger(__name__) + +def _extract_deliverables_from_messages( + messages: List[Dict], + module_description: str = "", + summarization_budget_chars: int = 0 +) -> Dict: + """ + Extract deliverables from an Associate's message history. + + This is a FALLBACK mechanism when an Associate calls finish_flow without + first calling generate_message_summary. The Associate SHOULD use the + summarization tool for quality, but if they don't, we extract their work + rather than losing it. + + DESIGN PRINCIPLES: + 1. NO TRUNCATION of individual findings - mission-critical content must be preserved + 2. PRIORITIZE recency - later messages usually contain conclusions + 3. ADAPTIVE to budget - if budget provided, be selective about WHICH findings to include + 4. PRESERVE FULL CONTENT of selected findings - never truncate mid-thought + + Args: + messages: The agent's message history + module_description: Description of the work module for context + summarization_budget_chars: Optional budget from Principal's briefing. + If provided, we SELECT fewer findings but keep them COMPLETE. + If 0 or not provided, include all substantive findings. + + Returns: + Dict with 'primary_summary' containing extracted findings + """ + if not messages: + return {} + + # Collect substantive content from assistant messages + findings = [] + tools_used = set() + + for msg in messages: + if msg.get("role") != "assistant": + continue + + content = msg.get("content", "") + tool_calls = msg.get("tool_calls", []) + + # Track tools used + for tc in tool_calls: + tool_name = tc.get("function", {}).get("name", "") + if tool_name: + tools_used.add(tool_name) + + # Extract meaningful content (skip very short or empty) + if content and len(content.strip()) > 50: + # Clean up the content - remove internal system markers only + cleaned = re.sub(r'.*?', '', content, flags=re.DOTALL) + cleaned = re.sub(r'.*?', '', cleaned, flags=re.DOTALL) + cleaned = re.sub(r'.*?', '', cleaned, flags=re.DOTALL) + cleaned = cleaned.strip() + + if cleaned and len(cleaned) > 30: + findings.append(cleaned) + + if not findings: + return {} + + # Selection strategy: NO TRUNCATION of content, but may SELECT fewer findings + # + # If budget is provided: + # - Prioritize the LAST N messages (conclusions are usually at the end) + # - Include complete findings that fit within budget + # - If a single finding exceeds budget, include it anyway (no truncation) + # + # If no budget: + # - Include all substantive findings (with reasonable max count) + + selected_findings = [] + total_chars = 0 + + if summarization_budget_chars and summarization_budget_chars > 0: + # Budget mode: be selective but preserve complete findings + # Reserve ~20% for formatting overhead + effective_budget = int(summarization_budget_chars * 0.80) + + # Work backwards (most recent = most likely to be conclusions) + for finding in reversed(findings): + finding_len = len(finding) + + # Always include at least ONE finding, even if it exceeds budget + if not selected_findings: + selected_findings.insert(0, finding) + total_chars += finding_len + continue + + # For subsequent findings, check budget + if total_chars + finding_len <= effective_budget: + selected_findings.insert(0, finding) + total_chars += finding_len + # else: skip this finding (but don't truncate it) + else: + # No budget: include all findings (reasonable max for sanity) + max_findings_unbounded = 15 # Prevent extreme cases + + # Still prioritize recent findings + for finding in reversed(findings[:max_findings_unbounded]): + selected_findings.insert(0, finding) + total_chars += len(finding) + + if not selected_findings: + return {} + + # Format the summary - findings are COMPLETE, not truncated + summary_parts = [] + + if module_description: + summary_parts.append(f"## Work Module: {module_description}\n") + + summary_parts.append("## Key Findings\n") + + for i, finding in enumerate(selected_findings, 1): + # Present each finding as a complete block + summary_parts.append(f"### Finding {i}\n{finding}\n") + + if tools_used: + summary_parts.append(f"\n## Tools Used\n- {', '.join(sorted(tools_used))}") + + # Add metadata about extraction + omitted_count = len(findings) - len(selected_findings) + if omitted_count > 0: + summary_parts.append(f"\n\n*Note: Auto-extracted {len(selected_findings)} of {len(findings)} work messages. " + f"{omitted_count} earlier messages omitted due to budget constraints. " + f"Full work log available in context_archive.*") + else: + summary_parts.append(f"\n\n*Note: Auto-extracted from complete work log ({len(findings)} messages)*") + + return { + "primary_summary": "\n".join(summary_parts) + } + @tool_registry( name="generate_message_summary", - description="Called by an Associate Agent when its work on a module is complete. This tool prepares a structured prompt for the agent to generate its final, comprehensive deliverable summary.", + description="Called by an Associate Agent when its work on a module is complete. This tool prepares a structured prompt for the agent to generate its final, comprehensive deliverable summary. The agent should respect any summarization budget provided in the briefing.", parameters={ "type": "object", "properties": { @@ -26,18 +164,49 @@ class GenerateMessageSummaryTool(BaseToolNode): """ A tool that generates an instructional prompt for an Associate Agent - to create its final deliverable summary. + to create its final deliverable summary. Supports adaptive summarization + based on budget provided by Principal. """ async def exec_async(self, prep_res: Dict) -> Dict: tool_params = prep_res.get("tool_params", {}) findings = tool_params.get("current_associate_findings") - + + # Get summarization budget from the agent's briefing context + sub_context = prep_res.get("sub_context", {}) + initial_briefing = sub_context.get("state", {}).get("initial_briefing", {}) + summarization_budget = initial_briefing.get("summarization_budget_chars", 0) + module_description = sub_context.get("meta", {}).get("module_description", "the assigned task") + + # Build adaptive summarization instructions + if summarization_budget and summarization_budget > 0: + budget_instruction = f""" +## SUMMARIZATION BUDGET: {summarization_budget:,} characters + +**CRITICAL**: The Principal has allocated approximately {summarization_budget:,} characters of context budget for your deliverable. +You MUST intelligently fit your summary within this budget by: +1. **Prioritize**: Keep details MOST pertinent to '{module_description}' in full detail. +2. **Summarize**: Condense less critical supporting information. +3. **Omit**: Drop tangential details that don't directly support the core findings. +4. **Reference**: For any omitted detail, note "Additional details available in work log" if important. + +Your final JSON 'primary_summary' value should be approximately {summarization_budget:,} characters or fewer. +""" + else: + budget_instruction = """ +## SUMMARIZATION: Full Detail Mode + +No character budget was specified. Provide your complete, detailed findings without summarization. +Include all relevant information, sources, and supporting details. +""" + instructional_prompt = f""" # FINALIZATION PROTOCOL INITIATED Your tactical work on this module is complete. Your final action is to synthesize all your work into a structured 'deliverables' package for the Principal Agent. +{budget_instruction} + ## Your Preliminary Findings (Provided by you): {findings} @@ -57,7 +226,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: In your next turn, provide ONLY the final JSON object in the 'content' field of your response. DO NOT call any tools. This will be your final output for this work module. """ - + return { "status": "success", "payload": { @@ -76,7 +245,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: } }}, ends_flow=True, - toolset_name="flow_control_end" + toolset_name="flow_control_end" ) class FinishNode(AsyncNode): async def prep_async(self, shared: Dict) -> Dict: @@ -85,24 +254,73 @@ async def prep_async(self, shared: Dict) -> Dict: if current_action: reason = current_action.get("reason", "No specific reason provided.") parent_agent_id_from_meta = shared['meta'].get("parent_agent_id") + + # Extract deliverables from message history for Associates + # Associates have a parent_agent_id (the Principal who dispatched them) + extracted_deliverables = {} + if parent_agent_id_from_meta: + # Check if Associate already produced structured deliverables + # (e.g., via generate_message_summary if they have flow_control_summary toolset) + existing_deliverables = shared.get("state", {}).get("deliverables", {}) + + if existing_deliverables and existing_deliverables.get("primary_summary"): + # Associate properly produced structured summary - use it + extracted_deliverables = existing_deliverables + logger.info("finish_flow_using_existing_deliverables", extra={ + "agent_id": shared.get("meta", {}).get("agent_id"), + "summary_length": len(existing_deliverables.get("primary_summary", "")) + }) + else: + # Associate skipped summarization - auto-extract as fallback + messages = shared.get("state", {}).get("messages", []) + module_description = shared.get("meta", {}).get("module_description", "") + + # Get summarization budget from initial briefing if available + initial_briefing = shared.get("state", {}).get("initial_briefing", {}) + summarization_budget = initial_briefing.get("summarization_budget_chars", 0) + + extracted_deliverables = _extract_deliverables_from_messages( + messages, + module_description, + summarization_budget_chars=summarization_budget + ) + + if extracted_deliverables: + logger.info("finish_flow_auto_extracted_deliverables", extra={ + "agent_id": shared.get("meta", {}).get("agent_id"), + "summary_length": len(extracted_deliverables.get("primary_summary", "")), + "budget_used": summarization_budget + }) + return { "reason": reason, "shared_sub_context": shared, - "parent_agent_id_for_event": parent_agent_id_from_meta + "parent_agent_id_for_event": parent_agent_id_from_meta, + "extracted_deliverables": extracted_deliverables } async def exec_async(self, prep_res: Dict) -> Dict: reason = prep_res.get("reason", "No specific reason provided.") + extracted_deliverables = prep_res.get("extracted_deliverables", {}) + logger.info("flow_ending", extra={"reason": reason}) + + # Use extracted deliverables if available, otherwise fall back to state.final_report + deliverables = extracted_deliverables + if not deliverables: + final_report = prep_res.get("shared_sub_context", {}).get('state', {}).get("final_report") + if final_report: + deliverables = {"final_report": final_report} + return { - "status": "flow_ending_initiated", + "status": "flow_ending_initiated", "reason": reason, "result_package": { "status": "COMPLETED_SUCCESSFULLY", "final_summary": f"Flow completed as instructed. Reason: {reason}", "terminating_tool": "finish_flow", "error_details": None, - "deliverables": {"final_report": prep_res.get("shared_sub_context", {}).get('state', {}).get("final_report")} + "deliverables": deliverables } } @@ -110,4 +328,8 @@ async def post_async(self, shared: Dict, prep_res: Any, exec_res: Dict) -> Optio current_sub_context = prep_res.get("shared_sub_context") if current_sub_context and exec_res and "result_package" in exec_res: current_sub_context["state"]["final_result_package"] = exec_res["result_package"] + # Also store deliverables in state for consistency + deliverables = exec_res.get("result_package", {}).get("deliverables", {}) + if deliverables: + current_sub_context["state"]["deliverables"] = deliverables return None diff --git a/core/agent_core/nodes/custom_nodes/get_principal_status_tool.py b/core/agent_core/nodes/custom_nodes/get_principal_status_tool.py index bb7fd6c..c829ab2 100644 --- a/core/agent_core/nodes/custom_nodes/get_principal_status_tool.py +++ b/core/agent_core/nodes/custom_nodes/get_principal_status_tool.py @@ -1,27 +1,100 @@ import logging -from typing import Dict +from typing import Dict, List, Tuple from pocketflow import AsyncNode from ...framework.tool_registry import tool_registry -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import uuid logger = logging.getLogger(__name__) +# Constants for staleness detection +STALE_ONGOING_THRESHOLD_MINUTES = 10 # If a module is "ongoing" with no update for this long, flag as stale +ORPHANED_SESSION_THRESHOLD_MINUTES = 30 # If ALL modules are stale, session is likely orphaned + @tool_registry( name="GetPrincipalStatusSummaryTool", description="Retrieves the current execution status summary and recent milestones of the Principal Agent. Use this to monitor progress.", parameters={"type": "object", "properties": {}}, # No parameters needed from LLM - toolset_name="monitoring_tools" + toolset_name="monitoring_tools" ) class GetPrincipalStatusSummaryTool(AsyncNode): """ - A tool for the Partner Agent to get the status summary and milestones + A tool for the Partner Agent to get the status summary and milestones from the Principal Agent's shared state. """ def __init__(self, **kwargs): super().__init__(**kwargs) # self.agent_id will be Partner's ID when this tool is run by Partner + def _detect_stale_modules(self, work_modules: Dict, now: datetime) -> Tuple[List[Dict], bool, str]: + """ + Detect modules that appear to be stale (stuck in ongoing/running state without recent updates). + + Returns: + - List of stale module info dicts + - Boolean indicating if session appears orphaned (all active modules stale) + - Warning message string (empty if no issues) + """ + stale_modules = [] + active_modules = [] # Modules in ongoing/in_progress state + + for module_id, module_data in work_modules.items(): + status = module_data.get('status', 'unknown') + updated_at_str = module_data.get('updated_at') + + # Track active (in-progress) modules + if status in ['ongoing', 'in_progress']: + active_modules.append(module_id) + + if updated_at_str: + try: + # Parse ISO format timestamp + if updated_at_str.endswith('Z'): + updated_at_str = updated_at_str[:-1] + '+00:00' + updated_at = datetime.fromisoformat(updated_at_str) + + # Ensure timezone aware + if updated_at.tzinfo is None: + updated_at = updated_at.replace(tzinfo=timezone.utc) + + time_since_update = now - updated_at + minutes_since_update = time_since_update.total_seconds() / 60 + + if minutes_since_update > STALE_ONGOING_THRESHOLD_MINUTES: + stale_modules.append({ + "module_id": module_id, + "status": status, + "minutes_since_update": round(minutes_since_update, 1), + "last_updated": updated_at_str + }) + except (ValueError, TypeError) as e: + logger.warning("stale_detection_timestamp_parse_error", + extra={"module_id": module_id, "updated_at": updated_at_str, "error": str(e)}) + + # Determine if session is orphaned (all active modules are stale) + is_orphaned = len(active_modules) > 0 and len(stale_modules) == len(active_modules) + + # Build warning message + warning_parts = [] + if stale_modules: + stale_ids = [m["module_id"] for m in stale_modules] + max_minutes = max(m["minutes_since_update"] for m in stale_modules) + + if is_orphaned: + warning_parts.append( + f"⚠️ CRITICAL: SESSION APPEARS ORPHANED - All {len(stale_modules)} active module(s) " + f"({', '.join(stale_ids)}) show status 'ongoing' but have had NO UPDATES for " + f"{round(max_minutes)} minutes. This session was likely interrupted by a WebSocket " + f"disconnect or server restart. The work shown as 'ongoing' is NOT actually running." + ) + else: + warning_parts.append( + f"⚠️ WARNING: {len(stale_modules)} module(s) ({', '.join(stale_ids)}) appear stale - " + f"showing 'ongoing' status but no updates for {round(max_minutes)}+ minutes." + ) + + return stale_modules, is_orphaned, "\n".join(warning_parts) + async def prep_async(self, partner_context: Dict) -> Dict: """ Preparation step. No specific input needed from LLM for this tool. @@ -31,7 +104,7 @@ async def prep_async(self, partner_context: Dict) -> Dict: run_ctx_global = partner_context['refs']['run'] # Access global RunContext if not run_ctx_global: return {"error": "Critical: Global RunContext not found in Partner's SubContext refs."} - + principal_sub_context_ref = run_ctx_global['sub_context_refs'].get("_principal_context_ref") # principal_flow_task_handle is stored in the RunContext.runtime principal_flow_task_handle = run_ctx_global['runtime'].get("principal_flow_task_handle") @@ -51,7 +124,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: principal_sub_context = prep_res.get("principal_sub_context_ref") # This is Principal's SubContext principal_task_handle = prep_res.get("principal_flow_task_handle") - + # Access team_state from partner_sub_context's refs partner_sub_context = prep_res.get("partner_sub_context_ref") team_state_global = partner_sub_context['refs']['team'] if partner_sub_context else None @@ -72,7 +145,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: principal_private_state = principal_sub_context["state"] # Principal's private state if not principal_private_state: return {"status": "error", "summary": "Principal Agent context is invalid (missing 'state').", "detailed_report": {"error": "Principal state invalid.", "is_principal_flow_running_in_team_state": is_principal_running_in_team_state}} - + principal_messages = principal_private_state.get("messages", []) # Principal's plan (now work_modules) is read from its refs.team (which is team_state_global) principal_work_modules = principal_sub_context['refs']['team'].get("work_modules", {}) @@ -93,7 +166,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: principal_task_handle_status_text = "Principal Task Handle Status: Running" else: principal_task_handle_status_text = "Principal Task Handle Status: Not launched or handle unavailable" - + # Effective running status text based on team_state effective_principal_running_status_text = f"Principal Effective Status (from team_state): {'Running' if is_principal_running_in_team_state else 'Not Running'}" @@ -104,7 +177,7 @@ async def exec_async(self, prep_res: Dict) -> Dict: if not is_principal_running_in_team_state and principal_task_handle and principal_task_handle.done() and \ not principal_task_handle.cancelled() and not principal_task_handle.exception(): is_marked_complete = True - + # More robust check: iterate messages for finish_flow tool call and success for msg_idx, msg in enumerate(reversed(principal_messages)): if msg.get("role") == "assistant" and msg.get("tool_calls"): @@ -131,35 +204,70 @@ async def exec_async(self, prep_res: Dict) -> Dict: part = f" - Msg {msg_idx + 1} [{role.upper()}]" # Adjusted message numbering if tool_name_resp: part += f" (Tool: {tool_name_resp})" - + if content: part += f": {str(content)}" # Use full content - + if tool_calls: tools_called_str = ", ".join([tc.get("function",{}).get("name","N/A") for tc in tool_calls]) part += f" -> Calls tool(s): [{tools_called_str}]" full_message_history_parts.append(part) - + full_message_history_text = "Full Principal Message History:\n" + "\n".join(full_message_history_parts) if full_message_history_parts else "Full Principal Message History:\n No messages recorded." - # 4. Format Work Modules + # 4. Detect stale/orphaned modules + now = datetime.now(timezone.utc) + stale_modules, is_session_orphaned, staleness_warning = self._detect_stale_modules(principal_work_modules, now) + + # 5. Format Work Modules (with staleness indicators) work_modules_summary_parts = ["Principal's Current Work Modules (from team_state):"] if principal_work_modules and isinstance(principal_work_modules, dict): for module_id, module_data in principal_work_modules.items(): module_name = module_data.get('name', 'Unnamed Module') module_status = module_data.get('status', 'unknown') module_desc_snippet = module_data.get('description', 'No description')[:50] + "..." - work_modules_summary_parts.append(f" - Module ID: {module_id}, Name: {module_name}, Status: {module_status}, Desc: {module_desc_snippet}") + updated_at = module_data.get('updated_at', 'N/A') + + # Check if this module is stale + is_stale = any(sm["module_id"] == module_id for sm in stale_modules) + stale_indicator = " ⚠️ STALE" if is_stale else "" + + work_modules_summary_parts.append( + f" - Module ID: {module_id}, Name: {module_name}, Status: {module_status}{stale_indicator}, " + f"Last Updated: {updated_at}, Desc: {module_desc_snippet}" + ) if not principal_work_modules: # Empty dict work_modules_summary_parts.append(" No work modules defined.") else: work_modules_summary_parts.append(" No work modules available or format is incorrect.") work_modules_summary_text = "\n".join(work_modules_summary_parts) - # 5. Combine for summary_for_llm - summary_for_llm = f"{effective_principal_running_status_text}\n{principal_task_handle_status_text}\n{is_principal_marked_complete_text}\n{work_modules_summary_text}\n{full_message_history_text}" - - # 6. Prepare detailed_report + # 6. Build comprehensive status summary + status_parts = [] + + # Add critical warning at the TOP if session is orphaned + if is_session_orphaned: + status_parts.append(staleness_warning) + status_parts.append("") # blank line for emphasis + + status_parts.extend([ + effective_principal_running_status_text, + principal_task_handle_status_text, + is_principal_marked_complete_text, + ]) + + # Add non-critical staleness warning after status if not orphaned + if staleness_warning and not is_session_orphaned: + status_parts.append(staleness_warning) + + status_parts.extend([ + work_modules_summary_text, + full_message_history_text + ]) + + summary_for_llm = "\n".join(status_parts) + + # 7. Prepare detailed_report (with staleness info) detailed_report = { "task_handle_status_raw": str(principal_task_handle), "principal_task_handle_status_text": principal_task_handle_status_text, @@ -167,15 +275,33 @@ async def exec_async(self, prep_res: Dict) -> Dict: "effective_principal_running_status_text": effective_principal_running_status_text, "is_principal_marked_complete_text": is_principal_marked_complete_text, "message_count": len(principal_messages), - "work_modules_snapshot_from_team_state_raw": principal_work_modules, # Changed from plan_snapshot + "work_modules_snapshot_from_team_state_raw": principal_work_modules, "full_message_history_raw": principal_messages, + # New staleness detection fields + "staleness_detection": { + "is_session_orphaned": is_session_orphaned, + "stale_modules": stale_modules, + "staleness_warning": staleness_warning, + "detection_timestamp": now.isoformat(), + } } + # Log warning if orphaned session detected + if is_session_orphaned: + logger.warning("orphaned_session_detected", extra={ + "stale_module_count": len(stale_modules), + "stale_modules": [m["module_id"] for m in stale_modules], + "max_minutes_stale": max(m["minutes_since_update"] for m in stale_modules) if stale_modules else 0 + }) + return { - "status": "success", - "summary_for_llm": summary_for_llm, + "status": "success" if not is_session_orphaned else "warning_orphaned_session", + "summary_for_llm": summary_for_llm, "detailed_report": detailed_report, - "ATTENTION": "DO NOT call this tool again in your next turn, unless user explicitly asks for update. This tool is designed to be called once a while, not every turn.", + "ATTENTION": "DO NOT call this tool again in your next turn, unless user explicitly asks for update. This tool is designed to be called once a while, not every turn." if not is_session_orphaned else + "⚠️ CRITICAL: This session appears to be ORPHANED. The modules shown as 'ongoing' are NOT actually running. " + "You MUST inform the user that this research was interrupted and the work was NOT completed. " + "Do NOT tell the user to 'wait' or that work is 'in progress' - it is NOT.", } async def post_async(self, partner_context: Dict, prep_res: Dict, exec_res: Dict): @@ -186,8 +312,8 @@ async def post_async(self, partner_context: Dict, prep_res: Dict, exec_res: Dict partner_private_state = partner_context["state"] tool_name = self._tool_info["name"] team_state_global = partner_context['refs']['team'] - is_error = exec_res.get("status") != "success" - + is_error = exec_res.get("status") not in ["success", "warning_orphaned_session"] + # --- Inbox Migration --- tool_result_payload = { "tool_name": tool_name, @@ -208,17 +334,17 @@ async def post_async(self, partner_context: Dict, prep_res: Dict, exec_res: Dict if not is_error: detailed_report = exec_res.get("detailed_report", {}) - + # Reconcile team_state.is_principal_flow_running if team_state_global: new_flag_value_based_on_report = False task_handle_status_text = detailed_report.get("principal_task_handle_status_text", "") - + if "Running" in task_handle_status_text: new_flag_value_based_on_report = True - + current_team_state_flag = team_state_global.get("is_principal_flow_running") - + if current_team_state_flag != new_flag_value_based_on_report: team_state_global["is_principal_flow_running"] = new_flag_value_based_on_report logger.info("team_state_reconciled", extra={"agent_id": partner_context['meta'].get('agent_id'), "new_flag_value": new_flag_value_based_on_report, "task_handle_status": task_handle_status_text, "old_value": current_team_state_flag}) @@ -229,6 +355,6 @@ async def post_async(self, partner_context: Dict, prep_res: Dict, exec_res: Dict else: error_message = exec_res.get("summary", "Unknown error fetching Principal status.") logger.error("tool_failed", extra={"agent_id": partner_context['meta'].get('agent_id'), "tool_name": tool_name, "error_message": error_message}) - + partner_private_state["current_action"] = None return "default" diff --git a/core/agent_core/nodes/mcp_proxy_node.py b/core/agent_core/nodes/mcp_proxy_node.py index 8c1487a..8c72899 100644 --- a/core/agent_core/nodes/mcp_proxy_node.py +++ b/core/agent_core/nodes/mcp_proxy_node.py @@ -5,23 +5,29 @@ # Import the new base class from .base_tool_node import BaseToolNode +from ..services.server_manager import reconnect_mcp_server logger = logging.getLogger(__name__) +# Maximum number of reconnection attempts before giving up +MAX_RECONNECT_ATTEMPTS = 2 + + class MCPProxyNode(BaseToolNode): """ [Refactored] A proxy node for transparently calling tools on a native MCP server. Now inherits from BaseToolNode, which encapsulates standard prep and post logic. + Supports automatic reconnection on connection failures. """ def __init__(self, unique_tool_name: str, original_tool_name: str, server_name: str, tool_info: Dict, **kwargs): # 1. [Core Fix] Set the _tool_info attribute before calling the parent class constructor self._tool_info = tool_info - + # 2. Now it's safe to call the parent's constructor, which will perform checks # Note: We no longer need max_retries and wait, as BaseToolNode handles them # But for safety, we pass them via kwargs super().__init__(**kwargs) - + # 3. Continue with this class's initialization logic self.unique_tool_name = unique_tool_name self.original_tool_name = original_tool_name @@ -30,47 +36,43 @@ def __init__(self, unique_tool_name: str, original_tool_name: str, server_name: # The prep_async method has been removed; its logic is now in exec_async - async def exec_async(self, prep_res: Dict) -> Dict[str, Any]: + async def _call_tool_with_reconnect(self, session_group, tool_params: Dict, attempt: int = 1) -> Dict[str, Any]: """ - [Refactored] This is the core execution logic for this tool node. - It follows the BaseToolNode contract, receiving prep_res and returning a standard result dictionary. - """ - # 1. Get standard input from prep_res - tool_params = prep_res.get("tool_params", {}) - shared_context = prep_res.get("shared_context", {}) - - # 2. Get specific resources needed by this node from shared_context - session_group = shared_context.get("runtime_objects", {}).get("mcp_session_group") - if not session_group: - error_msg = f"MCPProxyNode ({self.unique_tool_name}): Agent's context-specific MCP Session Group not found." - logger.error("mcp_proxy_session_group_not_found", extra={"unique_tool_name": self.unique_tool_name}) - return {"status": "error", "error_message": error_msg} + Internal method that attempts to call the MCP tool, with automatic reconnection on failure. - # 3. Execute the original business logic - logger.info("mcp_proxy_tool_call_begin", extra={"unique_tool_name": self.unique_tool_name}) - + Args: + session_group: The MCP session group to use + tool_params: Parameters to pass to the tool + attempt: Current attempt number (1-indexed) + + Returns: + Result dictionary with status and payload/error + """ try: # Call the MCP tool with a 60-second timeout result = await asyncio.wait_for( session_group.call_tool(self.original_tool_name, tool_params), - timeout=60.0 + timeout=60.0 ) - + if result is None or not hasattr(result, 'content'): - raise ValueError("MCP tool returned an invalid or null response.") + raise ValueError("MCP tool returned an invalid or null response.") - logger.info("mcp_proxy_tool_call_success", extra={"unique_tool_name": self.unique_tool_name, "original_tool_name": self.original_tool_name}) + logger.info("mcp_proxy_tool_call_success", extra={ + "unique_tool_name": self.unique_tool_name, + "original_tool_name": self.original_tool_name, + "attempt": attempt + }) # Extract content content_text = "" if result.content and len(result.content) > 0: content_item = result.content[0] if hasattr(content_item, 'text') and content_item.text is not None: - content_text = content_item.text + content_text = content_item.text else: - content_text = str(content_item) + content_text = str(content_item) - # 4. Construct a success return value that conforms to the BaseToolNode contract return { "status": "success", "payload": { @@ -79,17 +81,46 @@ async def exec_async(self, prep_res: Dict) -> Dict[str, Any]: "server_name": self.server_name, "response_preview": content_text } - # Note: _knowledge_items_to_add has been removed } except anyio.ClosedResourceError as e: - error_msg = f"The connection to the external service '{self.server_name}' required by the tool '{self.unique_tool_name}' was unexpectedly closed. This is a non-recoverable connection error." - logger.error("mcp_proxy_connection_closed", extra={"unique_tool_name": self.unique_tool_name}, exc_info=True) - # Construct a failure return value that conforms to the BaseToolNode contract + # Connection was closed - attempt reconnection if we haven't exceeded max attempts + if attempt < MAX_RECONNECT_ATTEMPTS: + logger.warning("mcp_proxy_connection_closed_attempting_reconnect", extra={ + "unique_tool_name": self.unique_tool_name, + "server_name": self.server_name, + "attempt": attempt, + "max_attempts": MAX_RECONNECT_ATTEMPTS + }) + + # Attempt to reconnect the server + reconnect_success = await reconnect_mcp_server(session_group, self.server_name) + + if reconnect_success: + logger.info("mcp_proxy_reconnect_success_retrying", extra={ + "unique_tool_name": self.unique_tool_name, + "server_name": self.server_name, + "attempt": attempt + 1 + }) + # Retry the tool call with the reconnected session + return await self._call_tool_with_reconnect(session_group, tool_params, attempt + 1) + else: + logger.error("mcp_proxy_reconnect_failed", extra={ + "unique_tool_name": self.unique_tool_name, + "server_name": self.server_name + }) + + # If we're here, either max attempts exceeded or reconnection failed + error_msg = f"The connection to the external service '{self.server_name}' required by the tool '{self.unique_tool_name}' was unexpectedly closed. Reconnection attempt{'s' if attempt > 1 else ''} failed." + logger.error("mcp_proxy_connection_closed_final", extra={ + "unique_tool_name": self.unique_tool_name, + "attempts_made": attempt + }, exc_info=True) + return { "status": "error", "error_message": error_msg, - "payload": { # More detailed error information can be placed in the payload for LLM analysis + "payload": { "type": "CRITICAL_CONNECTION_FAILURE", "summary": error_msg, "instruction_for_llm": ( @@ -99,13 +130,47 @@ async def exec_async(self, prep_res: Dict) -> Dict[str, Any]: ) } } + except asyncio.TimeoutError: error_msg = f"Call to native MCP tool '{self.original_tool_name}' (server: {self.server_name}) timed out." - logger.error("mcp_proxy_tool_timeout", extra={"unique_tool_name": self.unique_tool_name, "original_tool_name": self.original_tool_name, "server_name": self.server_name}) + logger.error("mcp_proxy_tool_timeout", extra={ + "unique_tool_name": self.unique_tool_name, + "original_tool_name": self.original_tool_name, + "server_name": self.server_name, + "attempt": attempt + }) return {"status": "error", "error_message": error_msg} + except Exception as e: error_msg = f"Error calling native MCP tool '{self.original_tool_name}' (server: {self.server_name}): {str(e)}" - logger.error("mcp_proxy_tool_call_error", extra={"unique_tool_name": self.unique_tool_name, "original_tool_name": self.original_tool_name, "server_name": self.server_name}, exc_info=True) + logger.error("mcp_proxy_tool_call_error", extra={ + "unique_tool_name": self.unique_tool_name, + "original_tool_name": self.original_tool_name, + "server_name": self.server_name, + "attempt": attempt + }, exc_info=True) + return {"status": "error", "error_message": error_msg} + + async def exec_async(self, prep_res: Dict) -> Dict[str, Any]: + """ + [Refactored] This is the core execution logic for this tool node. + It follows the BaseToolNode contract, receiving prep_res and returning a standard result dictionary. + Now includes automatic reconnection on connection failures. + """ + # 1. Get standard input from prep_res + tool_params = prep_res.get("tool_params", {}) + shared_context = prep_res.get("shared_context", {}) + + # 2. Get specific resources needed by this node from shared_context + session_group = shared_context.get("runtime_objects", {}).get("mcp_session_group") + if not session_group: + error_msg = f"MCPProxyNode ({self.unique_tool_name}): Agent's context-specific MCP Session Group not found." + logger.error("mcp_proxy_session_group_not_found", extra={"unique_tool_name": self.unique_tool_name}) return {"status": "error", "error_message": error_msg} + # 3. Execute the tool call with automatic reconnection support + logger.info("mcp_proxy_tool_call_begin", extra={"unique_tool_name": self.unique_tool_name}) + + return await self._call_tool_with_reconnect(session_group, tool_params) + # The post_async method has been removed because BaseToolNode handles it diff --git a/core/agent_core/services/server_manager.py b/core/agent_core/services/server_manager.py index c95ad9b..04d84c8 100644 --- a/core/agent_core/services/server_manager.py +++ b/core/agent_core/services/server_manager.py @@ -8,10 +8,12 @@ from mcp.client.session_group import ClientSessionGroup, StdioServerParameters, StreamableHttpParameters from .file_monitor import start_file_monitoring from ..iic.core.iic_handlers import BASE_DIR -from ..config.app_config import NATIVE_MCP_SERVERS +from ..config.app_config import get_native_mcp_servers # --- START: Modified code --- # Import initialize_registry from tool_registry from ..framework.tool_registry import initialize_registry +# Import session security initialization +from api.session import initialize_session_security # --- END: Modified code --- logger = logging.getLogger(__name__) @@ -33,7 +35,7 @@ async def lifespan_manager(app: FastAPI): [Modified] FastAPI's lifespan manager, implementing a better "discover and pool" logic. """ global MCP_SESSION_POOL - + # Re-configure logging after uvicorn potentially modified it try: from ..config.logging_config import setup_global_logging @@ -41,33 +43,40 @@ async def lifespan_manager(app: FastAPI): log_level = os.getenv("LOG_LEVEL", "INFO") log_file = os.getenv("LOG_FILE", None) setup_global_logging(log_level, log_file) - + # Additional suppression for MCP-related warnings import logging logging.getLogger("mcp").setLevel(logging.ERROR) logging.getLogger("mcp.client").setLevel(logging.ERROR) logging.getLogger("mcp.server").setLevel(logging.ERROR) - + logger.info("logging_reconfigured_in_lifespan", extra={"description": "Logging reconfigured in lifespan manager"}) except Exception as e: logger.error("logging_reconfiguration_failed", extra={"description": "Failed to reconfigure logging", "error": str(e)}) - + logger.info("application_startup_begin", extra={"description": "Initializing resources via lifespan manager"}) - - + + # Initialize session security (JWT + fingerprint cookies) + try: + initialize_session_security() + logger.info("session_security_initialized", extra={"description": "Session security manager initialized"}) + except Exception as e: + logger.error("session_security_init_failed", extra={"description": "Failed to initialize session security", "error": str(e)}) + raise + # 1. Initialize an empty session pool MCP_SESSION_POOL = asyncio.Queue() logger.info("mcp_session_pool_initialized", extra={"description": "MCP Session Pool initialized"}) - + # 2. Create a session group specifically for tool discovery logger.info("tool_discovery_session_create_begin", extra={"description": "Creating a session group for initial tool discovery"}) discovery_session_group = await initialize_mcp_session_for_context() - + # 3. Use this session group to initialize the tool registry if discovery_session_group: logger.info("tool_registry_init_begin", extra={"description": "Passing discovery session to initialize the tool registry"}) await initialize_registry(discovery_session_group, "agent_core/nodes/custom_nodes") - + # 4. [Core] Put the session group used for discovery directly into the pool as the first available resource await release_mcp_session_to_pool(discovery_session_group) logger.info("tool_discovery_session_pooled", extra={"description": "Tool discovery session has been successfully pooled for reuse"}) @@ -75,15 +84,25 @@ async def lifespan_manager(app: FastAPI): logger.warning("tool_discovery_session_failed", extra={"description": "Failed to create discovery session group. No native MCP tools will be registered"}) # Even if discovery fails, continue to initialize an empty registry await initialize_registry(None, "agent_core/nodes/custom_nodes") - + logger.info("file_monitor_start", extra={"description": "Starting file monitor", "directory": BASE_DIR}) start_file_monitoring(BASE_DIR, loop=asyncio.get_running_loop()) # Core of the Lifespan Manager: yield control to the FastAPI application yield - + # When the application shuts down, this code will be executed logger.info("application_shutdown_begin", extra={"description": "Cleaning up resources via lifespan manager"}) - + + # Signal all WebSocket connections to close gracefully + try: + from api.server import shutdown_event + shutdown_event.set() + logger.info("shutdown_event_signaled", extra={"description": "Signaled all WebSocket connections to close"}) + # Give WebSockets a moment to close gracefully + await asyncio.sleep(2) + except Exception as e: + logger.warning("shutdown_event_signal_failed", extra={"error": str(e)}) + # 5. When the application shuts down, clean up all remaining sessions in the pool if MCP_SESSION_POOL: logger.info("mcp_session_pool_cleanup_begin", extra={"description": "Closing idle MCP sessions from the pool", "session_count": MCP_SESSION_POOL.qsize()}) @@ -101,7 +120,7 @@ async def lifespan_manager(app: FastAPI): logger.error("mcp_session_pool_shutdown_error", extra={"description": "Error retrieving session from pool during shutdown", "error": str(e)}, exc_info=True) logger.info("mcp_session_pool_cleanup_complete", extra={"description": "All idle MCP sessions from the pool have been closed"}) - + logger.info("application_shutdown_complete", extra={"description": "Application shutdown complete"}) # --- START: Modified code - Remove get_session_group --- @@ -115,11 +134,12 @@ async def initialize_mcp_session_for_context() -> Optional[ClientSessionGroup]: Returns a connected ClientSessionGroup instance, or None on failure. """ logger.info("mcp_session_group_init_begin", extra={"description": "Initializing a new ClientSessionGroup for a specific agent context"}) - + session_group = ClientSessionGroup() - + + native_mcp_servers = get_native_mcp_servers() server_connections = [] - for name, conf in NATIVE_MCP_SERVERS.items(): + for name, conf in native_mcp_servers.items(): transport = conf.get("transport") params = None if transport == "stdio": @@ -142,7 +162,7 @@ async def initialize_mcp_session_for_context() -> Optional[ClientSessionGroup]: else: result_or_exc.server_name_from_config = server_name logger.info("mcp_server_connection_success", extra={"description": "Context-specific MCP connection successful", "server_name": server_name}) - + return session_group # --- START: New code - Session pool management functions --- @@ -179,7 +199,7 @@ async def acquire_mcp_session_from_pool(max_retries=3) -> Optional[ClientSession await session_group.__aexit__(None, None, None) except Exception: logger.warning("mcp_session_cleanup_limitation", extra={"description": "DUE to MCP SDK Limitation - this session can not gracefully exit. Accumulating too many this error might drain your resource, but it should be fine as far as you are not running this as a service"}) - + if not is_healthy: # If not healthy, continue the loop to try and get the next one continue @@ -204,3 +224,92 @@ async def release_mcp_session_to_pool(session_group: ClientSessionGroup): await MCP_SESSION_POOL.put(session_group) logger.info("mcp_session_released_to_pool", extra={"description": "MCP session released back to the pool", "pool_size": MCP_SESSION_POOL.qsize()}) # --- END: New code --- + + +async def reconnect_mcp_server(session_group: ClientSessionGroup, server_name: str) -> bool: + """ + Attempts to reconnect a specific MCP server within a session group. + + This handles the case where a server connection was lost (e.g., server restart, + network error) and needs to be re-established without recreating the entire session group. + + Args: + session_group: The ClientSessionGroup containing the dead connection + server_name: The name of the server to reconnect (from mcp.json config) + + Returns: + True if reconnection succeeded, False otherwise + """ + logger.info("mcp_server_reconnect_begin", extra={ + "description": "Attempting to reconnect to MCP server", + "server_name": server_name + }) + + try: + # 1. Get the server configuration + native_mcp_servers = get_native_mcp_servers() + if server_name not in native_mcp_servers: + logger.error("mcp_server_reconnect_config_not_found", extra={ + "description": "Server configuration not found for reconnection", + "server_name": server_name + }) + return False + + conf = native_mcp_servers[server_name] + transport = conf.get("transport") + + # 2. Build connection parameters + params = None + if transport == "stdio": + params = StdioServerParameters(command=conf["command"], args=conf["args"]) + elif transport == "http": + params = StreamableHttpParameters(url=conf["url"]) + else: + logger.error("mcp_server_reconnect_unsupported_transport", extra={ + "description": "Unsupported transport type for reconnection", + "transport": transport, + "server_name": server_name + }) + return False + + # 3. Find and disconnect the dead session for this server + dead_session = None + for session in list(session_group.sessions): + # Check if this session belongs to the server we want to reconnect + if hasattr(session, 'server_name_from_config') and session.server_name_from_config == server_name: + dead_session = session + break + + if dead_session: + try: + await session_group.disconnect_from_server(dead_session) + logger.info("mcp_server_dead_session_disconnected", extra={ + "description": "Dead session disconnected", + "server_name": server_name + }) + except Exception as e: + # Even if disconnect fails, continue with reconnection + logger.warning("mcp_server_disconnect_warning", extra={ + "description": "Warning during dead session disconnect (continuing anyway)", + "server_name": server_name, + "error": str(e) + }) + + # 4. Connect to the server with fresh connection + new_session = await session_group.connect_to_server(params) + new_session.server_name_from_config = server_name + + logger.info("mcp_server_reconnect_success", extra={ + "description": "Successfully reconnected to MCP server", + "server_name": server_name + }) + return True + + except Exception as e: + logger.error("mcp_server_reconnect_failed", extra={ + "description": "Failed to reconnect to MCP server", + "server_name": server_name, + "error": str(e) + }, exc_info=True) + return False + diff --git a/core/agent_core/state/management.py b/core/agent_core/state/management.py index fe1b563..7dc1365 100644 --- a/core/agent_core/state/management.py +++ b/core/agent_core/state/management.py @@ -101,7 +101,7 @@ def create_run_context( # The Partner's initial question should also be set in the team_state if run_context["team_state"]["question"] is None: run_context["team_state"]["question"] = initial_params.get("initial_user_query") - + partner_ctx = create_partner_context(run_context_ref=run_context, parent_agent_id=None) run_context["sub_context_refs"]["_partner_context_ref"] = partner_ctx logger.debug("partner_context_created", extra={"server_run_id": server_run_id}) @@ -115,7 +115,7 @@ def create_run_context( if instance_id: resolved_ids.append(instance_id) run_context["team_state"]["profiles_list_instance_ids"] = resolved_ids - + principal_ctx = create_principal_context(run_context_ref=run_context, parent_agent_id=None) run_context["sub_context_refs"]["_principal_context_ref"] = principal_ctx logger.debug("principal_context_created", extra={"server_run_id": server_run_id}) @@ -129,7 +129,7 @@ def create_run_context( def create_partner_context(run_context_ref: RunContext, parent_agent_id: Optional[str]) -> SubContext: """Creates the sub-context for the Partner Agent.""" agent_id = "Partner" - + # 1. Create private state partner_state = _create_flow_specific_state_template() @@ -152,17 +152,23 @@ def create_partner_context(run_context_ref: RunContext, parent_agent_id: Optiona "team": run_context_ref["team_state"], } } - + # 4. Populate Partner-specific initial state # The Partner needs to know all available Profiles to discuss with the user all_profiles = run_context_ref["config"]["agent_profiles_store"] # Filter based on the available_for_staffing flag staffing_available_instance_ids = [ - inst_id for inst_id, prof in all_profiles.items() + inst_id for inst_id, prof in all_profiles.items() if prof.get("is_active") and not prof.get("is_deleted") and prof.get("available_for_staffing") is True ] partner_state["profiles_list_instance_ids"] = staffing_available_instance_ids - + + logger.info("partner_context_profiles_populated", extra={ + "total_profiles": len(all_profiles), + "staffing_available_count": len(staffing_available_instance_ids), + "staffing_instance_ids": staffing_available_instance_ids[:5] if staffing_available_instance_ids else [] + }) + # Add the initial question to the message history to provide context for the user's conversation initial_query = run_context_ref["team_state"].get("question") if initial_query: @@ -171,13 +177,13 @@ def create_partner_context(run_context_ref: RunContext, parent_agent_id: Optiona return partner_context def create_principal_context( - run_context_ref: RunContext, + run_context_ref: RunContext, parent_agent_id: Optional[str], iteration_mode: str ) -> SubContext: """Creates the sub-context for the Principal Agent.""" agent_id = "Principal" - + principal_state = _create_flow_specific_state_template() principal_state["current_iteration_count"] = 1 @@ -195,7 +201,7 @@ def create_principal_context( "team": run_context_ref["team_state"], } } - + # Initial query is now handled via Handover Protocol and inbox, not direct injection. return principal_context @@ -206,7 +212,7 @@ def validate_context_state(context: SubContext) -> bool: This is a sanity check, not strict type validation. """ if not isinstance(context, dict): return False - + required_top_keys = {"meta", "state", "runtime_objects", "refs"} if not required_top_keys.issubset(context.keys()): logger.warning("context_validation_failed_missing_keys", extra={"found_keys": list(context.keys())}) @@ -215,7 +221,7 @@ def validate_context_state(context: SubContext) -> bool: if not isinstance(context["refs"], dict) or "run" not in context["refs"] or "team" not in context["refs"]: logger.warning("context_validation_failed_refs_malformed") return False - + if not isinstance(context["state"], dict): logger.warning("context_validation_failed_state_not_dict") return False @@ -230,6 +236,50 @@ def update_context_activity(context: SubContext): agent_id = context.get("meta", {}).get("agent_id", "Unknown") logger.warning("context_activity_update_failed", extra={"agent_id": agent_id}) + +def _refresh_profile_instance_ids_on_restore(target_context: RunContext): + """ + Refreshes profile instance IDs in restored state to match current server's UUIDs. + + Profile instance IDs (UUIDs) are regenerated on each server restart. When a run is + resumed after a restart, the stored `profiles_list_instance_ids` will contain stale + UUIDs that no longer exist in the current `agent_profiles_store`. This function + recalculates the list of available profiles using the current server's store. + + This affects: + - Partner's state.profiles_list_instance_ids (used for displaying available associates) + - team_state.profiles_list_instance_ids (used by Principal for team composition) + """ + current_profiles_store = target_context["config"]["agent_profiles_store"] + + # Recalculate staffing-available profile instance IDs from current server's store + staffing_available_instance_ids = [ + inst_id for inst_id, prof in current_profiles_store.items() + if prof.get("is_active") and not prof.get("is_deleted") and prof.get("available_for_staffing") is True + ] + + old_count = 0 + + # Update Partner's state if it exists + partner_context = target_context.get("sub_context_refs", {}).get("_partner_context_ref") + if partner_context and isinstance(partner_context.get("state"), dict): + old_ids = partner_context["state"].get("profiles_list_instance_ids", []) + old_count = len(old_ids) if old_ids else 0 + partner_context["state"]["profiles_list_instance_ids"] = staffing_available_instance_ids + logger.info("partner_profiles_refreshed_on_restore", extra={ + "old_count": old_count, + "new_count": len(staffing_available_instance_ids) + }) + + # Also update team_state if it has profiles_list_instance_ids + team_state = target_context.get("team_state", {}) + if "profiles_list_instance_ids" in team_state: + team_state["profiles_list_instance_ids"] = staffing_available_instance_ids + logger.info("team_state_profiles_refreshed_on_restore", extra={ + "new_count": len(staffing_available_instance_ids) + }) + + def _inject_restored_state(target_context: RunContext, restored_data: Dict): """ (Refactored for a safer approach) @@ -257,7 +307,7 @@ def _inject_restored_state(target_context: RunContext, restored_data: Dict): restored_sub_states = restored_data.get("sub_contexts_state", {}) if isinstance(restored_sub_states, dict): for context_key, state_data in restored_sub_states.items(): - + # Check if the target reference already exists (e.g., Partner Context is pre-created) target_sub_context_ref = target_context["sub_context_refs"].get(context_key) @@ -268,11 +318,11 @@ def _inject_restored_state(target_context: RunContext, restored_data: Dict): logger.info("sub_context_state_injected", extra={"context_key": context_key}) else: logger.warning("sub_context_state_not_dict", extra={"context_key": context_key}) - + elif isinstance(state_data, dict): # If the reference does not exist (is None), rebuild the entire SubContext object on-demand logger.info("sub_context_rebuilding_on_demand", extra={"context_key": context_key}) - + # Extract metadata from the restored state to build the new meta parent_agent_id = state_data.get("parent_agent_id") # agent_id should be inferred from the key or retrieved from the state @@ -291,7 +341,7 @@ def _inject_restored_state(target_context: RunContext, restored_data: Dict): "team": target_context["team_state"], } } - + # Place the rebuilt complete SubContext object back into run_context target_context["sub_context_refs"][context_key] = rebuilt_sub_context logger.info("sub_context_rebuilt", extra={"context_key": context_key}) @@ -299,6 +349,12 @@ def _inject_restored_state(target_context: RunContext, restored_data: Dict): else: logger.warning("sub_context_data_not_dict_cannot_rebuild", extra={"context_key": context_key}) + # 2.5 CRITICAL: Refresh profile instance IDs to match current server's UUIDs + # Profile instance IDs are regenerated on each server restart, so restored state + # will have stale IDs that don't exist in the current agent_profiles_store. + # We must refresh them to ensure the Partner can see available associates. + _refresh_profile_instance_ids_on_restore(target_context) + # 3. Subsequent cleanup logic (unchanged) # --- Post-injection Cleanup: Finalize any interrupted states from previous session --- team_state = target_context.get("team_state") @@ -311,18 +367,18 @@ def _inject_restored_state(target_context: RunContext, restored_data: Dict): turn["status"] = "interrupted" turn["error_details"] = "This action was active when the previous session ended and could not be completed." turn["end_time"] = datetime.now(timezone.utc).isoformat() - + # Deeper check for running LLM interactions within the turn llm_interaction = turn.get("llm_interaction") if llm_interaction and llm_interaction.get("status") == "running": llm_interaction["status"] = "error" llm_interaction.setdefault("error", {})["message"] = "LLM interaction was interrupted by session termination." - + for attempt in llm_interaction.get("attempts", []): if attempt.get("status") in ["pending", "running"]: attempt["status"] = "failed" attempt["error"] = "Run was interrupted during LLM stream." - + # Reset principal flow flag if team_state.get("is_principal_flow_running") is True: team_state["is_principal_flow_running"] = False diff --git a/core/agent_core/utils/content_selection.py b/core/agent_core/utils/content_selection.py new file mode 100644 index 0000000..2328721 --- /dev/null +++ b/core/agent_core/utils/content_selection.py @@ -0,0 +1,464 @@ +""" +Content Selection Utilities for Budget-Aware Context Inheritance. + +This module provides utilities for selecting content from completed work modules +within a token/character budget. It implements a two-tier selection strategy: + +1. Tier 1 (Preferred): Use deliverables.primary_summary (LLM-generated summary) +2. Tier 2 (Fallback): Select raw messages newest-first with hydration before measurement + +Design Principles: +- Never truncate individual messages - this loses coherence +- Always hydrate KB tokens BEFORE measuring size - prevents post-selection expansion +- Prefer LLM summaries when available - cleaner, no KB tokens to expand + +See docs/architecture/context-budget-management.md for detailed documentation. +""" + +import logging +from typing import Dict, List, Optional, Tuple, Union, Any + +logger = logging.getLogger(__name__) + +# ============================================================================== +# MODULE-LEVEL CONSTANTS +# ============================================================================== + +# Approximate characters per token for budget calculations +# This is a conservative estimate (actual varies by content type) +CHARS_PER_TOKEN = 4 + +# Fraction of target agent's context window reserved for inherited content +# 40% ensures sufficient headroom for the agent's own work +INHERITANCE_BUDGET_FRACTION = 0.40 + +# Strategy identifiers for logging and metadata +STRATEGY_LLM_SUMMARY = "llm_summary" +STRATEGY_NEWEST_FIRST = "newest_first" +STRATEGY_EMPTY = "empty" + + +# ============================================================================== +# BUDGET COMPUTATION +# ============================================================================== + +def compute_inheritance_budget_chars( + target_context_limit_tokens: int, + num_sources: int, + inheritance_fraction: float = INHERITANCE_BUDGET_FRACTION +) -> int: + """ + Compute per-source character budget for content inheritance. + + Formula: + pool_tokens = target_context_limit_tokens * inheritance_fraction + pool_chars = pool_tokens * CHARS_PER_TOKEN + per_source_budget = pool_chars // num_sources + + Args: + target_context_limit_tokens: The spawning agent's context window in tokens + (e.g., 200000 for claude-sonnet-4) + num_sources: Number of source modules in inherit_messages_from + inheritance_fraction: Fraction of context reserved for inheritance + (default: 0.40 = 40%) + + Returns: + Per-source character budget (integer) + + Example: + >>> compute_inheritance_budget_chars(200000, 2) + 160000 # (200K * 0.40 / 2) * 4 chars/token + """ + if num_sources <= 0: + logger.warning("inheritance_budget_invalid_sources", extra={ + "num_sources": num_sources, + "fallback": 0 + }) + return 0 + + pool_tokens = int(target_context_limit_tokens * inheritance_fraction) + pool_chars = pool_tokens * CHARS_PER_TOKEN + per_source_budget = pool_chars // num_sources + + logger.debug("inheritance_budget_computed", extra={ + "target_context_tokens": target_context_limit_tokens, + "inheritance_fraction": inheritance_fraction, + "pool_tokens": pool_tokens, + "pool_chars": pool_chars, + "num_sources": num_sources, + "per_source_budget_chars": per_source_budget + }) + + return per_source_budget + + +# ============================================================================== +# CONTENT SELECTION +# ============================================================================== + +def _estimate_message_chars(message: Dict) -> int: + """ + Estimate the character count of a message. + + Counts the 'content' field if present. For tool_calls, estimates based on + function name and arguments. + """ + total = 0 + + # Count content + content = message.get("content") + if content: + if isinstance(content, str): + total += len(content) + elif isinstance(content, list): + # Multi-modal content (list of content blocks) + for block in content: + if isinstance(block, dict): + total += len(str(block.get("text", ""))) + else: + total += len(str(block)) + else: + total += len(str(content)) + + # Count tool_calls + tool_calls = message.get("tool_calls", []) + for tc in tool_calls: + fn = tc.get("function", {}) + total += len(fn.get("name", "")) + total += len(str(fn.get("arguments", ""))) + + # Add overhead for role, etc. + total += 50 # Conservative overhead for message structure + + return total + + +def _select_messages_newest_first( + messages: List[Dict], + budget_chars: int, + source_id: str = "unknown" +) -> Tuple[List[Dict], Dict[str, Any]]: + """ + Select messages from newest to oldest until budget is exhausted. + + IMPORTANT: Messages should already be hydrated (KB tokens expanded) + before calling this function to ensure accurate size measurement. + + Args: + messages: List of message dicts (should be hydrated) + budget_chars: Character budget for selection + source_id: Identifier for logging + + Returns: + Tuple of (selected_messages, metadata) + - selected_messages: List of selected messages in original order + - metadata: Dict with selection statistics + """ + if not messages: + return [], { + "strategy": STRATEGY_EMPTY, + "chars_used": 0, + "items_selected": 0, + "items_available": 0, + "source_id": source_id + } + + # Work from newest to oldest + reversed_messages = list(reversed(messages)) + selected_indices = [] # Track original indices of selected messages + chars_used = 0 + + for idx, msg in enumerate(reversed_messages): + msg_chars = _estimate_message_chars(msg) + + # Always include at least one message if nothing selected yet + # (prevents returning empty when budget is very small) + if chars_used + msg_chars <= budget_chars or not selected_indices: + selected_indices.append(len(messages) - 1 - idx) # Convert back to original index + chars_used += msg_chars + + # If this single message already exceeds budget, stop here + if chars_used > budget_chars: + logger.debug("content_selection_single_message_exceeds_budget", extra={ + "source_id": source_id, + "message_chars": msg_chars, + "budget_chars": budget_chars + }) + break + else: + # Budget exhausted + break + + # Restore original order + selected_indices.sort() + selected_messages = [messages[i] for i in selected_indices] + + metadata = { + "strategy": STRATEGY_NEWEST_FIRST, + "chars_used": chars_used, + "items_selected": len(selected_messages), + "items_available": len(messages), + "source_id": source_id, + "budget_chars": budget_chars, + "oldest_selected_index": selected_indices[0] if selected_indices else None, + "newest_selected_index": selected_indices[-1] if selected_indices else None + } + + logger.debug("content_selection_newest_first_complete", extra=metadata) + + return selected_messages, metadata + + +def select_content_within_budget( + deliverables: Optional[Dict], + messages: List[Dict], + budget_chars: int, + source_id: str = "unknown" +) -> Tuple[Union[str, List[Dict]], Dict[str, Any]]: + """ + Two-tier content selection within a character budget. + + Strategy: + 1. Tier 1 (Preferred): If deliverables.primary_summary exists AND fits budget, + use it. LLM summaries are cleaner and don't contain KB tokens. + + 2. Tier 2 (Fallback): Select messages newest-first until budget is exhausted. + Messages MUST be hydrated before calling this function. + + Args: + deliverables: Dict containing 'primary_summary' and/or other fields + messages: List of message dicts (should be PRE-HYDRATED for accurate sizing) + budget_chars: Character budget for selection + source_id: Identifier for logging + + Returns: + Tuple of (selected_content, metadata) + - selected_content: Either summary string (Tier 1) OR list of messages (Tier 2) + - metadata: Dict with "strategy", "chars_used", "items_selected", etc. + + Note: + The caller is responsible for hydrating messages BEFORE calling this function. + If messages contain KB tokens that will be expanded later, the budget + calculation will be incorrect. + """ + logger.info("inheritance_content_selection_started", extra={ + "source_id": source_id, + "budget_chars": budget_chars, + "has_deliverables": deliverables is not None, + "message_count": len(messages) if messages else 0 + }) + + # Tier 1: Try primary_summary from deliverables + if deliverables: + summary = deliverables.get("primary_summary") + if summary and isinstance(summary, str): + summary_chars = len(summary) + + if summary_chars <= budget_chars: + metadata = { + "strategy": STRATEGY_LLM_SUMMARY, + "chars_used": summary_chars, + "items_selected": 1, + "items_available": len(messages) if messages else 0, + "source_id": source_id, + "budget_chars": budget_chars, + "summary_chars": summary_chars + } + logger.debug("content_selection_using_summary", extra=metadata) + return summary, metadata + else: + logger.debug("content_selection_summary_exceeds_budget", extra={ + "source_id": source_id, + "summary_chars": summary_chars, + "budget_chars": budget_chars, + "fallback": "newest_first" + }) + + # Tier 2: Fall back to message selection + if messages: + return _select_messages_newest_first(messages, budget_chars, source_id) + + # Nothing available + return [], { + "strategy": STRATEGY_EMPTY, + "chars_used": 0, + "items_selected": 0, + "items_available": 0, + "source_id": source_id, + "budget_chars": budget_chars + } + + +# ============================================================================== +# FORMATTING FOR BRIEFING +# ============================================================================== + +def format_inherited_content_for_briefing( + content: Union[str, List[Dict]], + metadata: Dict[str, Any], + source_id: str +) -> List[Dict]: + """ + Format selected content as messages suitable for injection into agent briefing. + + Args: + content: Either a summary string (from Tier 1) or list of messages (from Tier 2) + metadata: Selection metadata from select_content_within_budget + source_id: Identifier of the source work module + + Returns: + List of message dicts formatted for briefing injection. + Each message includes _internal metadata for observability. + """ + strategy = metadata.get("strategy", STRATEGY_EMPTY) + + if strategy == STRATEGY_EMPTY: + return [] + + if strategy == STRATEGY_LLM_SUMMARY: + # Wrap summary in a structured message + return [{ + "role": "user", + "content": f"[Context inherited from {source_id}]\n\n{content}", + "_internal": { + "_no_handover": True, # Don't pass this on to subsequent agents + "_inherited_from": source_id, + "_selection_strategy": strategy, + "_chars": metadata.get("chars_used", 0) + } + }] + + if strategy == STRATEGY_NEWEST_FIRST: + # Return messages with inheritance metadata + formatted = [] + for i, msg in enumerate(content): + formatted_msg = msg.copy() + # Add internal metadata + formatted_msg["_internal"] = formatted_msg.get("_internal", {}).copy() + formatted_msg["_internal"]["_no_handover"] = True + formatted_msg["_internal"]["_inherited_from"] = source_id + formatted_msg["_internal"]["_selection_strategy"] = strategy + formatted_msg["_internal"]["_selection_index"] = i + formatted.append(formatted_msg) + + # Add header message + header = { + "role": "user", + "content": ( + f"[Context inherited from {source_id}: " + f"{len(content)} messages selected, " + f"{metadata.get('chars_used', 0):,} chars]" + ), + "_internal": { + "_no_handover": True, + "_inherited_from": source_id, + "_is_header": True + } + } + + return [header] + formatted + + # Unknown strategy - return empty + logger.warning("format_inherited_content_unknown_strategy", extra={ + "strategy": strategy, + "source_id": source_id + }) + return [] + + +# ============================================================================== +# ASYNC HELPERS (for hydration integration) +# ============================================================================== + +async def hydrate_messages_for_selection( + messages: List[Dict], + knowledge_base: Any, + source_id: str = "unknown" +) -> List[Dict]: + """ + Hydrate messages by expanding KB tokens before content selection. + + This MUST be called before select_content_within_budget when using + Tier 2 (message selection) to ensure accurate size measurement. + + Args: + messages: List of message dicts (may contain KB tokens like <#CGKB-00042>) + knowledge_base: KnowledgeBase instance with hydrate_content method + source_id: Identifier for logging + + Returns: + List of hydrated messages with KB tokens expanded + """ + if not messages: + return [] + + if not knowledge_base: + logger.warning("hydrate_messages_no_kb", extra={ + "source_id": source_id, + "message_count": len(messages), + "warning": "Returning unhydrated messages" + }) + return messages + + hydrated = [] + for msg in messages: + hydrated_msg = msg.copy() + try: + content = msg.get("content") + if content: + hydrated_msg["content"] = await knowledge_base.hydrate_content(content) + except Exception as e: + logger.error("hydrate_messages_failed", extra={ + "source_id": source_id, + "error": str(e) + }, exc_info=True) + # Keep original content on error + hydrated.append(hydrated_msg) + + logger.debug("hydrate_messages_complete", extra={ + "source_id": source_id, + "message_count": len(hydrated) + }) + + return hydrated + + +async def select_inherited_content_with_hydration( + context_archive_entry: Dict, + budget_chars: int, + knowledge_base: Any, + source_id: str = "unknown" +) -> Tuple[Union[str, List[Dict]], Dict[str, Any]]: + """ + High-level convenience function that handles hydration and selection. + + This function: + 1. Extracts deliverables and messages from a context_archive entry + 2. Hydrates messages (expands KB tokens) + 3. Performs two-tier content selection + + Args: + context_archive_entry: Dict with 'messages' and 'deliverables' keys + (typically context_archive[-1] from a work module) + budget_chars: Character budget for selection + knowledge_base: KnowledgeBase instance + source_id: Identifier for logging + + Returns: + Tuple of (selected_content, metadata) + """ + deliverables = context_archive_entry.get("deliverables", {}) + messages = context_archive_entry.get("messages", []) + + # Hydrate messages first + hydrated_messages = await hydrate_messages_for_selection( + messages, knowledge_base, source_id + ) + + # Perform selection + return select_content_within_budget( + deliverables=deliverables, + messages=hydrated_messages, + budget_chars=budget_chars, + source_id=source_id + ) diff --git a/core/agent_profiles/handover_protocols/principal_to_associate_briefing.yaml b/core/agent_profiles/handover_protocols/principal_to_associate_briefing.yaml index da0de2a..1ccb651 100644 --- a/core/agent_profiles/handover_protocols/principal_to_associate_briefing.yaml +++ b/core/agent_profiles/handover_protocols/principal_to_associate_briefing.yaml @@ -1,5 +1,5 @@ protocol_name: principal_to_associate_briefing -version: 2.1 +version: 2.3 description: "Defines the context handover from Principal to an Associate Agent for a specific work module." # 1. context_parameters: Defines the parameters to be filled directly by the parent Agent's (Principal) LLM @@ -21,6 +21,25 @@ context_parameters: type: array description: "A list of other module IDs whose message history should be inherited as context." items: { "type": "string" } + summarization_budget_chars: + type: integer + x-handover-title: "Summarization Budget (Characters)" + description: | + The maximum character budget for this associate's final deliverable summary. + - If null or 0: No summarization required - return full detailed findings. + - If set to a positive value: Associate should intelligently summarize findings to fit within this budget, + prioritizing information most pertinent to the assignment while summarizing less critical details. + The Principal calculates this based on: (available_context_budget / num_parallel_assignments). + This enables adaptive summarization - the associate can decide what details to keep verbatim vs. summarize. + # Internal parameters set by DispatcherNode for budget-aware content inheritance + _preselected_inherited_messages: + type: array + x-internal: true + description: "Pre-selected inherited messages within budget (set by DispatcherNode, not LLM)" + _preselection_metadata: + type: object + x-internal: true + description: "Metadata about content pre-selection (strategies used, budget info)" required: ["module_id_to_assign", "assignment_specific_instructions", "shared_context_for_all_assignments"] # 2. inheritance: Defines the data that HandoverService needs to extract from the parent Agent's (Principal) context @@ -41,13 +60,26 @@ inheritance: x-handover-title: "Complete Rework/Iteration History" condition: "v['state.current_action.parameters.module_id_to_assign'] and len((v['team.work_modules.' + v['state.current_action.parameters.module_id_to_assign'] + '.context_archive'] or [])) > 0" + # NEW (v2.3): Budget-aware pre-selected content - PREFERRED when available + # DispatcherNode pre-selects content within budget before calling HandoverService + - from_source: + path: "state.current_action.parameters._preselected_inherited_messages" + as_payload_key: "inherited_messages" + x-handover-title: "Inherited Context (Budget-Limited)" + condition: "v['state.current_action.parameters._preselected_inherited_messages']" + schema: + x-note: "Content pre-selected by DispatcherNode using two-tier strategy (LLM summary or newest-first messages)" + + # FALLBACK (legacy): Raw message iteration - only used when pre-selection is NOT available + # This preserves backward compatibility but may cause budget issues - from_source: path_to_iterate: "team.work_modules.{{ module_id }}.context_archive[-1].messages" iterate_on: module_id: "state.current_action.parameters.inherit_messages_from" as_payload_key: "inherited_messages" - x-handover-title: "Inherited Messages from Other Modules" - condition: "v['state.current_action.parameters.inherit_messages_from']" + x-handover-title: "Inherited Messages from Other Modules (Legacy Fallback)" + # Only use this rule if inherit_messages_from is set AND no pre-selected content exists + condition: "v['state.current_action.parameters.inherit_messages_from'] and not v['state.current_action.parameters._preselected_inherited_messages']" # 3. target_inbox_item: Defines the metadata of the finally generated InboxItem target_inbox_item: diff --git a/core/agent_profiles/llm_configs/associate_llm.yaml b/core/agent_profiles/llm_configs/associate_llm.yaml index 8ee05b3..2f568c4 100644 --- a/core/agent_profiles/llm_configs/associate_llm.yaml +++ b/core/agent_profiles/llm_configs/associate_llm.yaml @@ -1,18 +1,18 @@ name: associate_llm base_llm_config: base_default_llm type: llm_config -description_for_human: "LLM config for tactical tasks (Associate Agents)." +description_for_human: "LLM config for tactical tasks (Associate Agents). Uses Claude Sonnet 4." config: - model: "openai/gemini-2.5-flash" - base_url: + model: "anthropic/claude-sonnet-4-20250514" + api_key: _type: "from_env" - var: "GEMINI_BRIDGE_URL" - default: "http://127.0.0.1:8765/v1" - required: false - api_key: "5678" + var: "ANTHROPIC_API_KEY" + required: true temperature: _type: "from_env" var: "AGENT_TEMPERATURE" - default: 0.5 + default: 0.4 required: false + max_retries: 2 + wait_seconds_on_retry: 3 diff --git a/core/agent_profiles/llm_configs/embedding_model.yaml b/core/agent_profiles/llm_configs/embedding_model.yaml index a13328f..f59ba07 100644 --- a/core/agent_profiles/llm_configs/embedding_model.yaml +++ b/core/agent_profiles/llm_configs/embedding_model.yaml @@ -1,14 +1,22 @@ +# ================================================================ +# DEPRECATED / UNUSED CONFIG +# ================================================================ +# This config file is NOT currently used by the system. +# The RAG embedding model is configured directly in rag_configs/*.yaml +# via the `emb_model_id` field (currently set to "jina-api"). +# +# This file is retained for potential future use if the embedding +# configuration is migrated to the llm_config system. +# ================================================================ + name: embedding_model base_llm_config: base_default_llm type: llm_config -description_for_human: "LLM config for text embedding models used in RAG. NOTE: The RAG feature is a work-in-progress and not yet a well-tested effort." +description_for_human: "[UNUSED] LLM config for text embedding models. See rag_configs/*.yaml for actual embedding configuration." config: model: _type: "from_env" var: "EMBEDDING_LLM" - default: "gemini/gemini-embedding-exp-03-07" # The model currently used in your code + default: "gemini/gemini-embedding-exp-03-07" # OUTDATED - actual system uses jina-api required: false - # Non-generic parameters like embedding_dims can be kept in the code, - # as it is closely related to the vector store configuration. - # Alternatively, it could be defined here and read by the code. For now, we only focus on the model itself. diff --git a/core/agent_profiles/llm_configs/fast_utils_llm.yaml b/core/agent_profiles/llm_configs/fast_utils_llm.yaml index cfb6fea..54d64c4 100644 --- a/core/agent_profiles/llm_configs/fast_utils_llm.yaml +++ b/core/agent_profiles/llm_configs/fast_utils_llm.yaml @@ -1,16 +1,14 @@ name: fast_utils_llm base_llm_config: base_default_llm type: llm_config -description_for_human: "LLM config for fast, low-cost utility tasks like naming or simple formatting." +description_for_human: "LLM config for fast, low-cost utility tasks like naming or simple formatting. Uses Claude 3.5 Haiku." config: - model: "openai/gemini-2.5-flash-lite" - base_url: + model: "anthropic/claude-3-5-haiku-20241022" + api_key: _type: "from_env" - var: "GEMINI_BRIDGE_URL" - default: "http://127.0.0.1:8765/v1" - required: false - api_key: "5678" + var: "ANTHROPIC_API_KEY" + required: true temperature: _type: "from_env" var: "FAST_UTILS_TEMPERATURE" diff --git a/core/agent_profiles/llm_configs/principal_llm.yaml b/core/agent_profiles/llm_configs/principal_llm.yaml index 321caa9..5c46926 100644 --- a/core/agent_profiles/llm_configs/principal_llm.yaml +++ b/core/agent_profiles/llm_configs/principal_llm.yaml @@ -1,22 +1,31 @@ name: principal_llm # Logical name, referenced by Agent Profile's llm_config_ref base_llm_config: base_default_llm # Inherits base configuration type: llm_config -description_for_human: "LLM config for high-level strategic tasks (Principal/Partner)." +description_for_human: "LLM config for high-level strategic tasks (Principal/Partner). Uses Claude Sonnet 4.5 with 1M context." config: - model: "openai/gemini-2.5-pro" - base_url: + model: "anthropic/claude-sonnet-4-5-20250929" + api_key: _type: "from_env" - var: "GEMINI_BRIDGE_URL" - default: "http://127.0.0.1:8765/v1" - required: false - api_key: "5678" - # _type: "from_env" - # var: "PRINCIPAL_LLM" - # required: false - # default: "gemini/gemini-2.5-pro-preview" + var: "ANTHROPIC_API_KEY" + required: true temperature: _type: "from_env" var: "PRINCIPAL_TEMPERATURE" required: false - default: 1.0 + default: 0.4 + max_retries: 2 + wait_seconds_on_retry: 5 + # Enable extended 1M context window for Claude Sonnet 4.5 + # Requires ANTHROPIC_EXTRA_HEADERS={"anthropic-beta": "context-1m-2025-08-07"} in .env + extra_headers: + _type: "from_env" + var: "ANTHROPIC_EXTRA_HEADERS" + required: false + default: null + # Explicit context limit - used by Context Budget Guardian + max_context_tokens: + _type: "from_env" + var: "PRINCIPAL_MAX_CONTEXT_TOKENS" + required: false + default: 1000000 diff --git a/core/agent_profiles/profiles/Associate_Analyst_Academic.yaml b/core/agent_profiles/profiles/Associate_Analyst_Academic.yaml index 25ee05b..71a3fe1 100644 --- a/core/agent_profiles/profiles/Associate_Analyst_Academic.yaml +++ b/core/agent_profiles/profiles/Associate_Analyst_Academic.yaml @@ -43,4 +43,4 @@ text_definitions: ### Phase 5: Finalize and Submit Deliverables - **Condition**: If the "Completion Check" is YES. - - **Action**: You MUST stop further analysis. Your final action is to synthesize all your key analytical conclusions into a clear, structured summary. Then, you MUST synthesis, providing this synthesis as the `current_associate_findings` parameter. \ No newline at end of file + - **Action**: You MUST stop further analysis. Your final action is to synthesize all your key analytical conclusions into a clear, structured summary. Then, you MUST provide this synthesis as the `current_associate_findings` parameter. diff --git a/core/agent_profiles/profiles/Associate_GenericExecutor_EN.yaml b/core/agent_profiles/profiles/Associate_GenericExecutor_EN.yaml index 7bbbfa4..3579a9b 100644 --- a/core/agent_profiles/profiles/Associate_GenericExecutor_EN.yaml +++ b/core/agent_profiles/profiles/Associate_GenericExecutor_EN.yaml @@ -5,5 +5,6 @@ description_for_human: "Executor agent responsible for carrying out specific tas tool_access_policy: allowed_toolsets: - # Specific toolsets for the Generic Executor can be defined here - - "G" # For example, allow it to search as well \ No newline at end of file + - "flow_control_end" + - "all_google_related_mcp_servers" # Google/Gemini tools (only if enabled in mcp.json) + - "all_user_specified_mcp_servers" # User-defined domain-specific servers \ No newline at end of file diff --git a/core/agent_profiles/profiles/Associate_LocalRAG_EN.yaml b/core/agent_profiles/profiles/Associate_LocalRAG_EN.yaml index d6644f4..159aa64 100644 --- a/core/agent_profiles/profiles/Associate_LocalRAG_EN.yaml +++ b/core/agent_profiles/profiles/Associate_LocalRAG_EN.yaml @@ -12,7 +12,8 @@ tool_access_policy: # Clarify its tool usage priority: internal first, then external allowed_toolsets: - "rag_tools" # Internal knowledge base query - - "G" # External web search (as a fallback) + - "all_user_specified_mcp_servers" # Only user-specified, non-core MCP servers + - "jina_search_and_visit" # External web search (secondary fallback) # Text Definitions (Core) text_definitions: @@ -29,38 +30,27 @@ text_definitions: - Analyze the task assigned by the Principal to identify the core question or information required. - Formulate a precise and effective query string suitable for a semantic search. - ### Phase 2: Prioritize Internal Knowledge (Mandatory First Step) - - You MUST **always** begin by using the `rag_query` tool to search the internal knowledge base. This is non-negotiable. - - Example: `print(rag_query(question="Key design principles of the 'PocketFlow' framework"))` + ### Phase 2: Knowledge Base and Tool Usage + - Use all available tools and knowledge bases to conduct thorough research. Begin with the internal knowledge base RAG or other available tool calls providing domain specific access, but do not rely exclusively on any single source or toolset. If the internal knowledge base or domain specific tool calls do not provide sufficient information, use other allowed and available resources to supplement your findings. ### Phase 3: Evaluate and Synthesize - - **Scenario A: Sufficient Information Found** - - If the `rag_query` tool returns relevant information, your task is to synthesize this information into a comprehensive answer. - - Proceed directly to Phase 5. - - **Scenario B: Insufficient or No Information Found** - - If the `rag_query` tool returns no relevant results or the information is clearly incomplete, you are authorized to use external search tools as a fallback. - - Proceed to Phase 4. + - Synthesize the findings from all sources into a comprehensive answer. - ### Phase 4: Fallback to External Search (Conditional) - - **Condition**: Only if Phase 3, Scenario B is met. - - **Action**: Use the `G_google_web_search` and `G_web_fetch` tools to find the required information from the public internet. - - Synthesize the findings from your web search. - - ### Phase 5: Deliver Findings - - Consolidate all your findings (either from the internal RAG system or external search) into a clear and well-structured report. - - **Final Action**: You MUST call the `handover_to_summary` tool. In the `current_associate_findings` parameter, provide your complete, synthesized answer. This concludes your task. + ### Phase 4: Deliver Findings + - Consolidate all your findings into a clear and well-structured report. + - **Final Action**: You MUST call the `generate_message_summary` tool. In the `current_associate_findings` parameter, provide your complete, synthesized answer. This concludes your task. # 3. Self-Reflection Prompt: Reinforce its SOP associate_self_reflection_on_no_tool_call: |- Observation: In your previous turn, you did not call any specific tool. Let's review the Standard Operating Procedure. Current Work Module: '{{ initial_params.module_description }}' - + Instruction: 1. **Check SOP**: Have I completed Phase 2 (calling `rag_query`)? If not, that is my immediate next step. 2. **Evaluate Current State**: If I have already received results from `rag_query`, I must now decide if the information is sufficient. - 3. **Next Action**: - - If the information is sufficient, or if I have already completed a web search, my only remaining action is to call `handover_to_summary` with my final report. - - If the internal information was insufficient, I should now use the `G_google_web_search` tool. + 3. **Next Action**: + - If the information is sufficient, or if I have already completed a web search, my only remaining action is to call `generate_message_summary` with my final report. + - If the internal information was insufficient, I should check `Seats` knowledge base first, then `web_search` as last resort. 4. Execute the most logical next step according to the SOP. diff --git a/core/agent_profiles/profiles/Associate_SmartRAG_EN.yaml b/core/agent_profiles/profiles/Associate_SmartRAG_EN.yaml index bc21fca..b9f9f0b 100644 --- a/core/agent_profiles/profiles/Associate_SmartRAG_EN.yaml +++ b/core/agent_profiles/profiles/Associate_SmartRAG_EN.yaml @@ -11,6 +11,7 @@ description_for_human: "[Internal Knowledge Expert] A specialized agent that dis tool_access_policy: allowed_toolsets: - "rag_tools" + - "all_user_specified_mcp_servers" # User-specified, non-core MCP servers for domain-specific knowledge # Text definitions have been significantly enhanced for clarity, robustness, and professionalism. text_definitions: @@ -22,12 +23,12 @@ text_definitions: Your Standard Operating Procedure (SOP) is a strict, sequential process. You MUST follow these phases in order. ### **Phase 1: DISCOVER - Map the Knowledge Landscape** - - Your **FIRST ACTION** in any task, without exception, is to call the `list_rag_sources` tool. - - This initial step is mandatory to understand the full scope of available information sources for this project. - - **Example Call:** `print(list_rag_sources())` + - Your **FIRST ACTION** in any task, without exception, is to call the `list_rag_sources` tool and, if available, any auto-discovery or listing tools provided by user-specified, non-core MCP servers to assess available domain-specific knowledge. + - This initial step is mandatory to understand the full scope of available information sources for this project, including both RAG sources and any pertinent domain-specific tools. + - **Example Call:** `print(list_rag_sources())` and, if available, `print(list_domain_tools())` ### **Phase 2: TRIAGE & DECIDE - Select the Right Source** - - Once you receive the list of sources from the `list_rag_sources` tool, you MUST perform a triage in your `` block. + - Once you receive the list of sources from the `list_rag_sources` tool and other pertinent domain-specific tools, you MUST perform a triage in your `` block. - **Your reasoning MUST be documented.** Clearly state your analysis of the user's question and how it maps to the description of one of the available sources. - **Decision Guideline:** - For questions about the **current project's specifics** (e.g., "our design goals", "yesterday's meeting notes"), you MUST select the source where `is_global: false`. This is typically `internal_project_docs`. @@ -35,9 +36,15 @@ text_definitions: - If no single source seems perfect, choose the one that is most likely to contain relevant information. **Do not query multiple sources at once unless absolutely necessary and justified.** ### **Phase 3: QUERY - Execute the Search** - - After making your decision, you MUST call the `rag_query` tool. - - You MUST use the `sources` parameter to target **only your selected source**. This is critical for efficiency and accuracy. + - After making your decision, you MUST call the `rag_query` tool or other pertinent and available domain specific searching tool. + - You MUST use the `sources` parameter when calling the 'rag_query' tool to target **only your selected source**. This is critical for efficiency and accuracy. Perform similar content scoping if available domain specific tools offer this feature. - **Crucial:** Formulate a precise, keyword-rich question for the `question` parameter to maximize search relevance. + + ⚠️ **CRITICAL: CONTEXT WINDOW MANAGEMENT** ⚠️ + - For **domain-specific MCP tools with pagination controls** (e.g., Seats tools), you MUST use **page_size=2** (not the default 10). + - This is essential to prevent context overflow errors. Start small, review results, then paginate if needed. + - Always prefer incremental retrieval over large batch requests. + - **Example Call (after deciding 'internal_project_docs' is best):** `print(rag_query(question="Key design principles of the 'PocketFlow' framework", sources=["internal_project_docs"]))` @@ -48,5 +55,5 @@ text_definitions: - **If `rag_query` returns no relevant results:** - You MUST state that you could not find the information in the specified knowledge base. Do not invent an answer. - **Final Step:** - - Once your analysis is complete, you MUST call the `handover_to_summary` tool. + - Once your analysis is complete, you MUST call the `generate_message_summary` tool. - Provide your complete, synthesized answer (or the "not found" statement) in the `current_associate_findings` parameter. This concludes your task. diff --git a/core/agent_profiles/profiles/Associate_WebSearcher_Academic.yaml b/core/agent_profiles/profiles/Associate_WebSearcher_Academic.yaml index daa7bd1..dc25753 100644 --- a/core/agent_profiles/profiles/Associate_WebSearcher_Academic.yaml +++ b/core/agent_profiles/profiles/Associate_WebSearcher_Academic.yaml @@ -1,5 +1,5 @@ name: Associate_WebSearcher_Academic -base_profile: Associate_WebSearcher +base_profile: Associate_WebSearcher_EN description_for_human: "[Academic Literature Researcher] Specializes in systematic academic literature retrieval and evaluation. Proficient in using academic databases like Google Scholar, JSTOR, and Web of Science; skilled in keyword construction, bidirectional citation tracking (snowballing), and cross-database searching. Ideal for providing comprehensive literature support for reviews, theoretical research, and scientific exploration." # [Core] We only override text_definitions, consolidating all instructions into a powerful agent_role_intro. @@ -17,8 +17,8 @@ text_definitions: **(Note: For your very first action, since there is no "previous step", you may skip the "Analysis" part and start directly with "Plan for Next Step", basing your plan on the initial instructions from the Principal.)** 1. **Analysis of Previous Step:** Start by verbosely analyzing the result of your last action. Example: "The search on 'generative AI in education' returned 15 results. The top 3 from 'JSTOR' and 'ACM Digital Library' appear to be peer-reviewed articles directly addressing the topic. I will prioritize these." - 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, to verify the credibility and get detailed findings, my next plan is to use the `G_web_fetch` tool on the top three promising links." - 3. **Action:** At the end of your response, call the appropriate tool (`G_google_web_search` or `G_web_fetch`). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. + 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, to verify the credibility and get detailed findings, my next plan is to use the most appropriate specialized tool available (such as `visit_url`, `web_search`, or an academic knowledge base tool) on the top three promising links." + 3. **Action:** At the end of your response, call the most appropriate tool for your research (this may include `web_search`, `visit_url`, or any specialized academic knowledge base tool available). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. --- # ACTIONABLE FIELD MANUAL: ACADEMIC RESEARCH @@ -31,7 +31,7 @@ text_definitions: 4. **Targeted Author/Institution Search:** Identify and specifically search for the works of key scholars and institutions in the field. 5. **Cross-Database Search:** Execute searches in at least 3-5 core academic databases (e.g., Google Scholar, JSTOR, Web of Science, Scopus, PubMed). 6. **Adhere to Specified Timeframes:** You MUST prioritize any time constraints explicitly stated in the Principal's instructions. If no timeframe is given, you should intelligently adapt your search strategy based on the nature of the task (e.g., whether it asks for "recent developments" or "historical context"). - + ### B. Information Quality Vetting (Pre-Visit Filtering) **Before deciding to use `G_web_fetch` on any link, you MUST mentally vet the search result (title, snippet, source) against these criteria to decide if it's worth fetching:** - **Authority:** **EXTREMELY IMPORTANT.** (e.g., Peer-review status, journal reputation, author's background). @@ -44,4 +44,4 @@ text_definitions: 1. **Executive Summary:** A one or two-sentence summary of the core answer. 2. **Key Findings:** A bulleted list detailing important theories, findings, and arguments. Each point must **directly quote the key information** and cite the source (e.g., Author, Year or URL). 3. **Key Literature:** A list of the most important source URLs or DOIs. - 4. **Open Questions/Gaps:** Any unresolved contradictions or clear knowledge gaps identified. \ No newline at end of file + 4. **Open Questions/Gaps:** Any unresolved contradictions or clear knowledge gaps identified. diff --git a/core/agent_profiles/profiles/Associate_WebSearcher_Business.yaml b/core/agent_profiles/profiles/Associate_WebSearcher_Business.yaml index aff3bcc..78a93d8 100644 --- a/core/agent_profiles/profiles/Associate_WebSearcher_Business.yaml +++ b/core/agent_profiles/profiles/Associate_WebSearcher_Business.yaml @@ -1,11 +1,11 @@ name: Associate_WebSearcher_Business -base_profile: Associate_WebSearcher +base_profile: Associate_WebSearcher_EN description_for_human: "[Business Intelligence Analyst] Specialized in gathering information for business strategy analysis. Excels at mining market size, competitive landscapes, company intelligence, and industry trends from professional reports, financial statements, and news platforms." text_definitions: agent_role_intro: |- # ROLE: Autonomous Business Intelligence Analyst - # MISSION: Your sole mission is to understand a research directive from the Principal, and then independently conduct a multi-step investigation using the `G_google_web_search` and `G_web_fetch` tools to gather business-critical intelligence. + # MISSION: Your sole mission is to understand a research directive from the Principal, and then independently conduct a multi-step investigation using your available search tools (such as `web_search`, `visit_url`, or any specialized business knowledge base tool) to gather business-critical intelligence. agent_responsibilities: |- # YOUR CORE WORKFLOW & PRINCIPLES @@ -15,8 +15,8 @@ text_definitions: **(Note: For your very first action, since there is no "previous step", you may skip the "Analysis" part and start directly with "Plan for Next Step", basing your plan on the initial instructions from the Principal.)** 1. **Analysis of Previous Step:** Start by verbosely analyzing the result of your last action. Example: "The search for 'NVIDIA data center revenue Q2 2024' yielded a press release and several financial news articles. The press release is the most reliable source for the exact figures." - 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, I will use `G_web_fetch` on the official press release link to extract the exact revenue numbers and growth percentages." - 3. **Action:** At the end of your response, call the appropriate tool (`G_google_web_search` or `G_web_fetch`). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. + 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, I will use the most appropriate specialized tool available (such as `visit_url`, `web_search`, or a business knowledge base tool) on the official press release link to extract the exact revenue numbers and growth percentages." + 3. **Action:** At the end of your response, call the most appropriate tool for your research (this may include `web_search`, `visit_url`, or any specialized business knowledge base tool available). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. --- # ACTIONABLE FIELD MANUAL: BUSINESS INTELLIGENCE diff --git a/core/agent_profiles/profiles/Associate_WebSearcher_EN.yaml b/core/agent_profiles/profiles/Associate_WebSearcher_EN.yaml index caf2795..eecbf80 100644 --- a/core/agent_profiles/profiles/Associate_WebSearcher_EN.yaml +++ b/core/agent_profiles/profiles/Associate_WebSearcher_EN.yaml @@ -1,22 +1,24 @@ -name: Associate_WebSearcher +name: Associate_WebSearcher_EN base_profile: Base_Associate available_for_staffing: true -description_for_human: "Associate agent specialized in web searching tasks." +description_for_human: "Associate agent specialized in web searching tasks. Has access to all configured MCP servers and Jina search tools." tool_access_policy: - # This list will replace the list from the parent profile + # Toolset categories are resolved dynamically based on mcp.json 'enabled' status allowed_toolsets: - - "G" # This is the toolset for Gemini CLI backend, disable this if you are not using Gemini CLI as provider - # - "jina_search_and_visit" # This is the toolset for Jina search, uncomment this if you want to use Jina for web search and visit. + - "jina_search_and_visit" # Built-in Jina web search + - "all_google_related_mcp_servers" # Google/Gemini tools (only if enabled in mcp.json) + - "all_user_specified_mcp_servers" # User-defined domain-specific servers text_definitions: associate_self_reflection_on_no_tool_call: |- - Observation: In your previous turn, you did not call the `G_google_web_search` or `G_web_fetch` tool. + Observation: In your previous turn, you did not call any search or visit tool. Instruction: 1. Review your "Action Framework". This likely means your research is complete. 2. If it is, you MUST synthesize your findings and call the `generate_message_summary` tool to submit your work. This is your final step. - 3. If you are stuck or believe you need to search more but are unsure how, explain your reasoning and then call `G_google_web_search` with your best attempt at a new query. + 3. If you are stuck or believe you need to search more but are unsure how, explain your reasoning and then use one of your available search tools. + 4. **IMPORTANT**: Check your available MCP server tools first - they may have curated knowledge bases with high-quality, relevant content including real-life scenarios and training materials. system_prompt_construction: @@ -29,7 +31,16 @@ system_prompt_construction: order: 10 content: |- # ROLE: Autonomous Research Specialist - # MISSION: Your sole mission is to understand a research directive from the Principal, and then independently conduct a multi-step investigation using the `G_google_web_search` and `G_web_fetch` tools. You are expected to formulate queries, analyze results, and iteratively deepen the research until the objective is met. + # MISSION: Your sole mission is to understand a research directive from the Principal, and then independently conduct a multi-step investigation using your available search tools. You are expected to formulate queries, analyze results, and iteratively deepen the research until the objective is met. + + ## Knowledge Base and Tool Usage Guidance: + Unless otherwise instructed, use any specialized MCP knowledge bases or non-core toolsets to enrich and supplement your broad research and synthesis, not to replace it. Always maintain generality and breadth in your research, ensuring that access to specialized resources does not narrow your perspective or exclude broader sources of information. + + ## TOOL USAGE: + Use all available tools and knowledge bases to conduct thorough research. Do not rely exclusively on any single source or toolset. Combine information from specialized and general sources to provide the most comprehensive and accurate results possible. + + ## ADAPTIVE SUMMARIZATION: + If a summarization budget was provided in your briefing, you MUST respect it when creating your final deliverable. Prioritize information most relevant to your assignment. - id: agent_responsibilities type: static_text @@ -44,12 +55,12 @@ system_prompt_construction: 1. **Analysis of Previous Step:** Start by verbosely analyzing the result of your last action... - + 2. **Plan for Next Step:** Based on your analysis, clearly state what your next action will be... 3. **Action:** - At the end of your response, call the appropriate tool (`G_google_web_search` or `G_web_fetch`). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. + At the end of your response, call the appropriate search or visit tool. If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. # segment 4: Inject tool descriptions (new in L2) - id: associate_tools_available diff --git a/core/agent_profiles/profiles/Associate_WebSearcher_Technical.yaml b/core/agent_profiles/profiles/Associate_WebSearcher_Technical.yaml index e3c81db..db91f37 100644 --- a/core/agent_profiles/profiles/Associate_WebSearcher_Technical.yaml +++ b/core/agent_profiles/profiles/Associate_WebSearcher_Technical.yaml @@ -1,12 +1,18 @@ name: Associate_WebSearcher_Technical -base_profile: Associate_WebSearcher +base_profile: Associate_WebSearcher_EN description_for_human: "[Technical Intelligence Engineer] Specialized in gathering information for technical evaluation and selection. Proficient in consulting official documentation, GitHub, developer communities, and technical blogs to conduct comparative research and assess technology maturity." text_definitions: # Override the role, positioning as a "Technical Intelligence Engineer" agent_role_intro: |- # ROLE: Autonomous Technical Intelligence Engineer - # MISSION: Your sole mission is to understand a technical evaluation directive from the Principal, and then independently conduct a multi-step investigation using the `G_google_web_search` and `G_web_fetch` tools to gather precise technical information. + # MISSION: Your sole mission is to understand a technical evaluation directive from the Principal, and then independently conduct a multi-step investigation using your available search tools to gather precise technical information. + + ## Knowledge Base and Tool Usage Guidance: + Unless otherwise instructed, use any specialized MCP knowledge bases or non-core toolsets to enrich and supplement your broad research and synthesis, not to replace it. Always maintain generality and breadth in your research, ensuring that access to specialized resources does not narrow your perspective or exclude broader sources of information. + + ## TOOL USAGE: + Use all available tools and knowledge bases to conduct thorough research. Do not rely exclusively on any single source or toolset. Combine information from specialized and general sources to provide the most comprehensive and accurate results possible. # Override responsibilities, injecting the "action manual" for technical selection agent_responsibilities: |- @@ -18,9 +24,9 @@ text_definitions: 1. **Analysis of Previous Step:** Start by verbosely analyzing the result of your last action. Example: "The search for 'PostgreSQL vs. MongoDB performance' led me to a benchmark article on a reputable tech blog. I will now visit this URL." or "Visiting the official documentation for v16.1 confirmed the new feature set, which is crucial for my analysis." - 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, I will now use `G_web_fetch` to read the benchmark article to extract performance metrics." or "I have now gathered performance data, architectural overviews, and community feedback. I will proceed to finalization." + 2. **Plan for Next Step:** Based on your analysis and the "ACTIONABLE FIELD MANUAL" below, clearly state what your next action will be and why. Example: "Therefore, I will now use the most appropriate specialized tool available (such as `visit_url`, `web_search`, or a technical knowledge base tool) to read the benchmark article to extract performance metrics." or "I have now gathered performance data, architectural overviews, and community feedback. I will proceed to finalization." - 3. **Action:** At the end of your response, call the appropriate tool (`G_google_web_search` or `G_web_fetch`). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. + 3. **Action:** At the end of your response, call the most appropriate tool for your research (this may include `web_search`, `visit_url`, or any specialized technical knowledge base tool available). If you have determined that the research is complete, you MUST call the `generate_message_summary` tool instead. --- # ACTIONABLE FIELD MANUAL: TECHNICAL INTELLIGENCE diff --git a/core/agent_profiles/profiles/Base_Agent.yaml b/core/agent_profiles/profiles/Base_Agent.yaml index 4676662..0fa55e3 100644 --- a/core/agent_profiles/profiles/Base_Agent.yaml +++ b/core/agent_profiles/profiles/Base_Agent.yaml @@ -14,16 +14,16 @@ system_prompt_construction: order: 2 - id: must_reply type: static_text - content: "You must reply to every user message, referably with a function call to a tool. " + content: "You must reply to every user message, preferably with a function call to a tool. " order: 3 - id: tool_caller type: static_text content: | - In this environment you have access to a set of tools you can use to answer the user's question. - You can use one or more tools per message, and will receive the result of that tool use in the user's response. + In this environment you have access to a set of tools you can use to answer the user's question. + You can use one or more tools per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use. order: 4 - + - id: bonus type: static_text content: "Now Begin! If you solve the task correctly, your team will receive a reward of $1,000,000, if you fail, you might got fired by your Managing Director." diff --git a/core/agent_profiles/profiles/Base_Associate_EN.yaml b/core/agent_profiles/profiles/Base_Associate_EN.yaml index f3882f7..9d50d5c 100644 --- a/core/agent_profiles/profiles/Base_Associate_EN.yaml +++ b/core/agent_profiles/profiles/Base_Associate_EN.yaml @@ -34,6 +34,7 @@ llm_config_ref: "associate_llm" tool_access_policy: allowed_toolsets: - "flow_control_end" + - "flow_control_summary" # Gives all Associates the CHOICE to summarize before returning system_prompt_construction: diff --git a/core/agent_profiles/profiles/Partner_StrategicAdvisor_EN.yaml b/core/agent_profiles/profiles/Partner_StrategicAdvisor_EN.yaml index 09e2e40..127d39d 100644 --- a/core/agent_profiles/profiles/Partner_StrategicAdvisor_EN.yaml +++ b/core/agent_profiles/profiles/Partner_StrategicAdvisor_EN.yaml @@ -7,12 +7,12 @@ is_active: true is_deleted: false timestamp: "2025-05-27T10:00:00.000Z" # Please replace with a real timestamp description_for_human: "Strategic advisor AI that interacts with the user, defines research scope, and (later) launches Principal, monitors progress, and facilitates iterative research." -llm_config_ref: "principal_llm" # Partner might use a powerful LLM +llm_config_ref: "principal_llm" base_profile: Base_Agent system_prompt_construction: # strategy_path, system_prompt_template_path, user_prompt_templates_map are replaced by segments. - + system_prompt_segments: - id: partner_mission_and_workflow type: static_text @@ -21,6 +21,9 @@ system_prompt_construction: ## Your Mission: AI Research Partner & Strategist You are a sophisticated AI assistant acting as a strategic partner to the user, mirroring the workflow of a top-tier consultant. Your primary mission is to deconstruct every user request, conduct initial research to ground the strategy in evidence, and then propose a structured, hypothesis-driven research plan for the user's approval before launching a full-scale research effort. + ## Knowledge Base and Tool Usage Guidance: + Unless otherwise instructed, use any specialized MCP knowledge bases or non-core toolsets to enrich and supplement your broad research and synthesis, not to replace it. Always maintain generality and breadth in your research, ensuring that access to specialized resources does not narrow your perspective or exclude broader sources of information. + ## Your Guiding Principles: * **Be Proactive:** Always ask clarifying questions to fully understand the user's goals. * **Always Plan:** Treat every request as an opportunity for deep analysis. Your goal is not to give a quick answer but to architect a comprehensive research plan. @@ -29,7 +32,7 @@ system_prompt_construction: ## Your Standard Workflow: ### Step 1: Understand & Initial Search - * Acknowledge the user's request and using `G_google_web_search` to gather initial context and understand the problem space. + * Acknowledge the user's request and use the most appropriate specialized tool available (such as a web search, visit, or knowledge base tool) to gather initial context and understand the problem space. ### Step 2: Propose the Research Framework * Based on your initial findings, acknowledge that the question requires deeper analysis. @@ -39,8 +42,8 @@ system_prompt_construction: ### Step 3: Get Confirmation & Execute * Once the user agrees with your proposed plan, use the `manage_work_modules` tool to formalize it. * Discuss the required expert team composition (available Associate profiles). - * Finally, after all preparations are confirmed, call `LaunchPrincipalExecutionTool` to start the Principal Agent. - + * Finally, after all preparations are confirmed, call `LaunchPrincipalExecutionTool` to start the Principal Agent. + - id: partner_framework_example_guidance type: static_text order: 20 @@ -57,14 +60,14 @@ system_prompt_construction: - id: partner_tools type: tool_description order: 30 - - id: partner_available_associates_list - type: state_value + - id: partner_available_associates_list + type: state_value source_state_path: "state.profiles_list_instance_ids" # The state. prefix is still valid - ingestor_id: "available_associates_ingestor" + ingestor_id: "available_associates_ingestor" condition: "get_nested_value_from_context(context_obj, \"state.profiles_list_instance_ids\") and len(get_nested_value_from_context(context_obj, \"state.profiles_list_instance_ids\")) > 0" # Use V-Model path - # wrapper_tags: ["", ""] - ingestor_params: - title: "### Available Associate Agent Profiles for Team Configuration" + # wrapper_tags: ["", ""] + ingestor_params: + title: "### Available Associate Agent Profiles for Team Configuration" order: 45 # Place it after tools, before final reminders - id: partner_final_reminders type: static_text @@ -106,7 +109,7 @@ pre_turn_observers: payload: content_key: "partner_guidance_on_principal_completion" consumption_policy: "consume_on_read" - + - id: "observer_on_profile_update" type: "declarative" condition: "any(item.get('source') == 'PROFILES_UPDATED_NOTIFICATION' for item in get_nested_value_from_context(context_obj, 'state.inbox', []))" @@ -137,11 +140,15 @@ pre_turn_observers: # At the top level of Partner_StrategicAdvisor_EN.yaml tool_access_policy: - # Keep the original toolsets - allowed_toolsets: ["planning_tools", "monitoring_tools", "intervention_tools", "G"] + allowed_toolsets: + - "planning_tools" + - "monitoring_tools" + - "intervention_tools" + - "all_google_related_mcp_servers" + - "all_user_specified_mcp_servers" allowed_individual_tools: - "LaunchPrincipalExecutionTool" - - "GetPrincipalStatusSummaryTool" + - "GetPrincipalStatusSummaryTool" - "SendDirectiveToPrincipalTool" @@ -162,13 +169,13 @@ text_definitions: partner_guidance_on_principal_completion: |- The Principal Agent you launched has just completed its task. The internal event `...` in your message history contains the final status and details. - + Your next actions are: 1. **Analyze the Outcome**: Review the event details and the full project context (plan, previous tool results, etc.). You can use the `GetPrincipalStatusSummaryTool` if you need more detailed information than what's provided in the event. 2. **Summarize for User**: Formulate a concise and clear summary of the Principal's execution results for the user. 3. **Report to User**: Present this summary to the user in a new message. 4. **Await Next Steps**: After reporting, await the user's feedback or next instructions for potential iteration or a new task. - + partner_framework_example: |- Here is an example of a high-quality, hypothesis-driven framework for your inspiration. You can adapt its structure to best fit the user's specific problem: > **1. Core Question: How sustainable is Nvidia's current market leadership? (The 'Core Issue')** diff --git a/core/agent_profiles/profiles/Principal_Planner_EN.yaml b/core/agent_profiles/profiles/Principal_Planner_EN.yaml index f9b7660..fce55b0 100644 --- a/core/agent_profiles/profiles/Principal_Planner_EN.yaml +++ b/core/agent_profiles/profiles/Principal_Planner_EN.yaml @@ -6,13 +6,14 @@ is_active: true is_deleted: false timestamp: "2025-05-27T10:00:00.000Z" description_for_human: "Orchestrator responsible for high-level planning, task decomposition, and overall research direction." -llm_config_ref: "principal_llm" # Principal uses a powerful LLM +llm_config_ref: "principal_llm" tool_access_policy: allowed_toolsets: - "planning_tools" - "reporting_tools" - "flow_control_end" + - "all_user_specified_mcp_servers" # Domain-specific detail for strategic planning allowed_individual_tools: - "dispatch_submodules" - "generate_markdown_report" @@ -57,10 +58,10 @@ system_prompt_construction: ingestor_id: "available_associates_ingestor" condition: "get_nested_value_from_context(context_obj, 'team.profiles_list_instance_ids')" ingestor_params: - title: "### Associate Agent Profiles Available for Task Dispatch" + title: "### Associate Agent Profiles Available for Task Dispatch" order: 40 - id: principal_tools - type: tool_description + type: tool_description order: 50 - id: principal_constraints type: static_text @@ -75,14 +76,25 @@ system_prompt_construction: * **Follow Workflow:** Strictly adhere to the illustrated workflow, especially the iteration loop. * **Tool Utilization:** You can use tools like `StagePlanner` to assist in your planning and management. When using the `dispatch_submodules` tool, ensure you are assigning tasks that are currently in 'pending' status in your plan. + ## Knowledge Base and Tool Usage Guidance: + Unless otherwise instructed, use any specialized MCP knowledge bases or non-core toolsets to enrich and supplement your broad research and synthesis, not to replace it. Always maintain generality and breadth in your research, ensuring that access to specialized resources does not narrow your perspective or exclude broader sources of information. + ## Important Note on Tool Usage: The following tools are for Associate Agent use ONLY. You (Principal) are strictly forbidden from directly calling these tools. This list is provided solely for your reference when assigning tasks and instructions to Associates. If you need to call them, use `dispatch_submodules` to launch an Associate Agent to complete the corresponding task. - + ## Instructions for using `dispatch_submodules`: * The `dispatch_submodules` tool takes an `assignments` array. Each object in this array represents one Work Module to be assigned to a specific Associate profile. * **CRITICAL: You SHOULD group all modules that can be executed in parallel into a SINGLE call to this tool.** To achieve concurrent execution, provide multiple assignment objects within the `assignments` array. Only dispatch modules sequentially if they have a strict dependency on the results of a previous module. - * **Example of a parallel dispatch call:** + + ## ADAPTIVE SUMMARIZATION BUDGET: + When dispatching multiple modules in parallel, you SHOULD provide a `summarization_budget_chars` for each assignment to prevent context overflow when results return. + - **Calculate budget**: Estimate your available context budget for results (e.g., 50,000 chars), divide by number of parallel assignments. + - **Set per-assignment**: Add `"summarization_budget_chars": ` to each assignment object. + - **Omit for full detail**: If you need complete, un-summarized output from an assignment, omit this field or set to 0. + - The Associate will intelligently summarize to fit the budget while preserving the most pertinent information. + + * **Example of a parallel dispatch call with summarization budgets:** ```json { "tool_name": "dispatch_submodules", @@ -91,13 +103,15 @@ system_prompt_construction: "module_id_to_assign": "wm_abc123de", "assigned_role_name": "Financial Analyst", "agent_profile_logical_name": "Associate_WebSearcher", - "principal_provided_context_for_assignment": "Focus on financial performance from official reports. Cross-reference with market analysis articles." + "assignment_specific_instructions": "Focus on financial performance from official reports. Cross-reference with market analysis articles.", + "summarization_budget_chars": 15000 }, { "module_id_to_assign": "wm_fgh456jk", "assigned_role_name": "Market Expansion Scout", "agent_profile_logical_name": "Associate_WebSearcher", - "principal_provided_context_for_assignment": "Find recent news about market expansion, particularly in the Asian market. Summarize the top 3 key announcements." + "assignment_specific_instructions": "Find recent news about market expansion, particularly in the Asian market. Summarize the top 3 key announcements.", + "summarization_budget_chars": 15000 } ], "shared_context_for_all_assignments": "The overall research goal is to compare the market positions of Company A and Company B." @@ -108,6 +122,7 @@ system_prompt_construction: * `assigned_role_name` (REQUIRED): You MUST provide a custom, descriptive role name for this instance (e.g., 'Market Researcher', 'Technical Analyst'). This name will be used in the UI to identify the agent's purpose. * `agent_profile_logical_name`: Specify the logical name of the Associate Agent Profile to use (e.g., "Associate_WebSearcher"). * `assignment_specific_instructions` (REQUIRED): You MUST provide specific, actionable instructions for this assignment. For rework, detail what needs to be fixed. + * `summarization_budget_chars` (RECOMMENDED): Character budget for the associate's final deliverable. Calculate as: available_context / num_parallel_assignments. Omit for full detail. * `inherit_deliverables_from` (OPTIONAL): To inherit **conclusions/summaries**, provide a list of module IDs here. Use this when the Associate needs the results of other modules. * `inherit_messages_from` (OPTIONAL): To inherit the **full conversational history** for deep context, provide a list of module IDs here. Use this when the Associate needs to see the *process* of how a result was obtained, not just the result itself. * You can also provide a `shared_context_for_all_assignments` string if there's common background information relevant to all assignments in this call. @@ -115,11 +130,11 @@ system_prompt_construction: ## Specialized Task Delegation: * **For website creation**: For tasks that require generating HTML, CSS, or JavaScript files, you should dispatch the task to the `Associate_WebDeveloper` profile. This agent is specialized in creating web content and using the `write_file` tool to save the results. - + ## Guidance for Principal Agent: * **Before calling any tool**: Always analyze the current state of the project, * Be verbose before calling a tool, explaining what you see in previous messages and what you plan to do next. - * ATTENTION: DO NOT call tools without a clear, actionable plan. DO NOT call tools without a verbose explanation on what you see and what you plan to do next. + * ATTENTION: DO NOT call tools without a clear, actionable plan. DO NOT call tools without a verbose explanation on what you see and what you plan to do next. - id: principal_system_tool_contributions type: tool_contributed_context order: 70 @@ -171,9 +186,9 @@ post_turn_observers: type: "add_to_inbox" target_agent_id: "self" inbox_item: - source: "SELF_REFLECTION_PROMPT" + source: "SELF_REFLECTION_PROMPT" payload: - content_key: "principal_replan_guidance" + content_key: "principal_replan_guidance" consumption_policy: "consume_on_read" flow_decider: @@ -221,4 +236,4 @@ text_definitions: * **FINALIZE:** If all information has been gathered, call `generate_markdown_report`. 3. **Explain Your New Strategy** clearly in your reasoning before acting. - + diff --git a/core/api/connection_manager.py b/core/api/connection_manager.py new file mode 100644 index 0000000..ab86418 --- /dev/null +++ b/core/api/connection_manager.py @@ -0,0 +1,755 @@ +""" +Connection Manager for WebSocket Resilience + +This module provides resilient WebSocket connection management with: +- Heartbeat (ping/pong) mechanism to detect stale connections +- Reconnection grace period to survive temporary disconnects +- Event buffering during disconnection for replay on reconnect +- Run context persistence for resumability + +Architecture: + + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” heartbeat β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Client │◄──────────────► β”‚ ConnectionManagerβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ β”‚ β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β” + β”‚ RunContext β”‚ β”‚ EventBuffer β”‚ β”‚ HeartbeatMgr β”‚ + β”‚ (persistent) β”‚ β”‚ (per run) β”‚ β”‚ (per socket) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +""" + +import asyncio +import logging +import time +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from enum import Enum +from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING +from collections import deque + +if TYPE_CHECKING: + from fastapi import WebSocket + from api.events import SessionEventManager + +logger = logging.getLogger(__name__) + + +# ============================================================================= +# Configuration Constants +# ============================================================================= + +class ConnectionConfig: + """Configuration for connection resilience.""" + + # Heartbeat settings + HEARTBEAT_INTERVAL_SECONDS: float = 30.0 # Send ping every 30 seconds + HEARTBEAT_TIMEOUT_SECONDS: float = 10.0 # Expect pong within 10 seconds + MAX_MISSED_HEARTBEATS: int = 3 # Disconnect after 3 missed pongs + + # Reconnection settings + RECONNECTION_GRACE_PERIOD_SECONDS: float = 120.0 # 2 minutes to reconnect + + # Event buffering settings + MAX_BUFFERED_EVENTS: int = 1000 # Max events to buffer per run + EVENT_BUFFER_TTL_SECONDS: float = 300.0 # Events expire after 5 minutes + + # Checkpoint settings + CHECKPOINT_ON_TURN_COMPLETE: bool = True # Save state after each turn + + +# ============================================================================= +# Connection State +# ============================================================================= + +class ConnectionState(Enum): + """WebSocket connection state.""" + CONNECTED = "connected" + DISCONNECTED = "disconnected" + RECONNECTING = "reconnecting" + GRACE_PERIOD = "grace_period" + TERMINATED = "terminated" + + +@dataclass +class HeartbeatState: + """Tracks heartbeat state for a connection.""" + last_ping_sent: Optional[datetime] = None + last_pong_received: Optional[datetime] = None + missed_heartbeats: int = 0 + heartbeat_task: Optional[asyncio.Task] = None + + def record_ping(self): + """Record that a ping was sent.""" + self.last_ping_sent = datetime.now() + + def record_pong(self): + """Record that a pong was received.""" + self.last_pong_received = datetime.now() + self.missed_heartbeats = 0 + + def record_missed(self) -> int: + """Record a missed heartbeat and return total missed count.""" + self.missed_heartbeats += 1 + return self.missed_heartbeats + + +@dataclass +class BufferedEvent: + """An event buffered during disconnection.""" + timestamp: datetime + event_type: str + event_data: Dict[str, Any] + run_id: Optional[str] = None + + def is_expired(self, ttl_seconds: float) -> bool: + """Check if this event has expired.""" + return (datetime.now() - self.timestamp).total_seconds() > ttl_seconds + + +@dataclass +class RunConnectionState: + """Connection state for a specific run.""" + run_id: str + session_id: str + state: ConnectionState = ConnectionState.CONNECTED + + # Timing + connected_at: datetime = field(default_factory=datetime.now) + disconnected_at: Optional[datetime] = None + grace_period_expires: Optional[datetime] = None + + # Event buffer for replay on reconnect + event_buffer: deque = field(default_factory=lambda: deque(maxlen=ConnectionConfig.MAX_BUFFERED_EVENTS)) + + # Associated tasks (not cancelled during grace period) + tasks: Dict[str, asyncio.Task] = field(default_factory=dict) + + # Checkpoint data + last_checkpoint: Optional[datetime] = None + checkpoint_data: Optional[Dict[str, Any]] = None + + def start_grace_period(self): + """Start the reconnection grace period.""" + self.state = ConnectionState.GRACE_PERIOD + self.disconnected_at = datetime.now() + self.grace_period_expires = datetime.now() + timedelta( + seconds=ConnectionConfig.RECONNECTION_GRACE_PERIOD_SECONDS + ) + logger.info("grace_period_started", extra={ + "run_id": self.run_id, + "session_id": self.session_id, + "expires_at": self.grace_period_expires.isoformat(), + "grace_seconds": ConnectionConfig.RECONNECTION_GRACE_PERIOD_SECONDS + }) + + def is_grace_period_expired(self) -> bool: + """Check if grace period has expired.""" + if self.state != ConnectionState.GRACE_PERIOD: + return False + if self.grace_period_expires is None: + return True + return datetime.now() > self.grace_period_expires + + def reconnect(self): + """Mark as reconnected.""" + self.state = ConnectionState.CONNECTED + self.disconnected_at = None + self.grace_period_expires = None + logger.info("run_reconnected", extra={ + "run_id": self.run_id, + "session_id": self.session_id, + "buffered_events": len(self.event_buffer) + }) + + def buffer_event(self, event_type: str, event_data: Dict[str, Any]): + """Buffer an event for later replay.""" + event = BufferedEvent( + timestamp=datetime.now(), + event_type=event_type, + event_data=event_data, + run_id=self.run_id + ) + self.event_buffer.append(event) + + def get_buffered_events(self, clear: bool = True) -> List[BufferedEvent]: + """Get buffered events, optionally clearing the buffer.""" + # Filter out expired events + ttl = ConnectionConfig.EVENT_BUFFER_TTL_SECONDS + valid_events = [e for e in self.event_buffer if not e.is_expired(ttl)] + + if clear: + self.event_buffer.clear() + + return valid_events + + def save_checkpoint(self, checkpoint_data: Dict[str, Any]): + """Save a checkpoint.""" + self.last_checkpoint = datetime.now() + self.checkpoint_data = checkpoint_data + logger.debug("checkpoint_saved", extra={ + "run_id": self.run_id, + "checkpoint_time": self.last_checkpoint.isoformat() + }) + + +# ============================================================================= +# Connection Manager +# ============================================================================= + +class ConnectionManager: + """ + Manages WebSocket connections with resilience features. + + This is a singleton that tracks all active connections and run states, + providing heartbeat monitoring, reconnection support, and event buffering. + """ + + _instance: Optional['ConnectionManager'] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + # Run states keyed by run_id + self._run_states: Dict[str, RunConnectionState] = {} + + # Heartbeat states keyed by session_id + self._heartbeat_states: Dict[str, HeartbeatState] = {} + + # WebSocket references keyed by session_id + self._websockets: Dict[str, 'WebSocket'] = {} + + # Event managers keyed by session_id + self._event_managers: Dict[str, 'SessionEventManager'] = {} + + # Mapping from run_id to session_id for lookups + self._run_to_session: Dict[str, str] = {} + + # Grace period monitor task + self._monitor_task: Optional[asyncio.Task] = None + + self._initialized = True + logger.info("connection_manager_initialized") + + # ------------------------------------------------------------------------- + # Connection Lifecycle + # ------------------------------------------------------------------------- + + def register_connection( + self, + session_id: str, + websocket: 'WebSocket', + event_manager: 'SessionEventManager' + ): + """Register a new WebSocket connection.""" + self._websockets[session_id] = websocket + self._event_managers[session_id] = event_manager + self._heartbeat_states[session_id] = HeartbeatState() + + logger.info("connection_registered", extra={ + "session_id": session_id + }) + + def unregister_connection(self, session_id: str): + """Unregister a WebSocket connection (but don't terminate runs yet).""" + # Start grace period for all runs on this session + runs_in_grace = [] + for run_id, run_state in self._run_states.items(): + if run_state.session_id == session_id: + if run_state.state == ConnectionState.CONNECTED: + run_state.start_grace_period() + runs_in_grace.append(run_id) + + # Remove websocket reference but keep run states + self._websockets.pop(session_id, None) + + # Stop heartbeat + heartbeat_state = self._heartbeat_states.pop(session_id, None) + if heartbeat_state and heartbeat_state.heartbeat_task: + heartbeat_state.heartbeat_task.cancel() + + logger.info("connection_unregistered", extra={ + "session_id": session_id, + "runs_in_grace_period": runs_in_grace + }) + + # Ensure monitor is running + self._ensure_monitor_running() + + return runs_in_grace + + def register_run(self, run_id: str, session_id: str) -> RunConnectionState: + """Register a new run on a session.""" + run_state = RunConnectionState( + run_id=run_id, + session_id=session_id, + state=ConnectionState.CONNECTED + ) + self._run_states[run_id] = run_state + self._run_to_session[run_id] = session_id + + logger.info("run_registered", extra={ + "run_id": run_id, + "session_id": session_id + }) + + return run_state + + def get_run_state(self, run_id: str) -> Optional[RunConnectionState]: + """Get the connection state for a run.""" + return self._run_states.get(run_id) + + def is_run_active(self, run_id: str) -> bool: + """Check if a run is still active (connected or in grace period).""" + run_state = self._run_states.get(run_id) + if not run_state: + return False + return run_state.state in (ConnectionState.CONNECTED, ConnectionState.GRACE_PERIOD) + + def can_reconnect(self, run_id: str) -> bool: + """Check if a run can be reconnected to.""" + run_state = self._run_states.get(run_id) + if not run_state: + return False + if run_state.state == ConnectionState.GRACE_PERIOD: + return not run_state.is_grace_period_expired() + return False + + async def reconnect_run( + self, + run_id: str, + new_session_id: str, + websocket: 'WebSocket', + event_manager: 'SessionEventManager' + ) -> Dict[str, Any]: + """ + Reconnect to an existing run during grace period. + + Returns: + Dict with 'success' bool and additional info: + - success: True if reconnection succeeded + - error: Error message if failed + - buffered_events: List of replayed events if success + - events_replayed: Count of replayed events + """ + run_state = self._run_states.get(run_id) + + if not run_state: + logger.warning("reconnect_failed_no_run", extra={"run_id": run_id}) + return { + "success": False, + "error": f"Run {run_id} not found in connection manager" + } + + if not self.can_reconnect(run_id): + logger.warning("reconnect_failed_not_in_grace", extra={ + "run_id": run_id, + "state": run_state.state.value + }) + return { + "success": False, + "error": f"Run {run_id} is not in reconnectable state (state: {run_state.state.value})" + } + + # Update connection references + old_session_id = run_state.session_id + run_state.session_id = new_session_id + run_state.reconnect() + + # Update mappings + self._run_to_session[run_id] = new_session_id + self._websockets[new_session_id] = websocket + self._event_managers[new_session_id] = event_manager + self._heartbeat_states[new_session_id] = HeartbeatState() + + # Get buffered events before replay + buffered_events = run_state.get_buffered_events(clear=False) + events_count = len(buffered_events) + + logger.info("run_reconnected_success", extra={ + "run_id": run_id, + "old_session_id": old_session_id, + "new_session_id": new_session_id, + "buffered_events": events_count + }) + + # Replay buffered events + await self._replay_buffered_events(run_id, event_manager) + + return { + "success": True, + "buffered_events": [ + {"type": e.event_type, "data": e.event_data, "timestamp": e.timestamp.isoformat()} + for e in buffered_events + ], + "events_replayed": events_count, + } + + async def _replay_buffered_events( + self, + run_id: str, + event_manager: 'SessionEventManager' + ): + """Replay buffered events to a reconnected client.""" + run_state = self._run_states.get(run_id) + if not run_state: + return + + events = run_state.get_buffered_events(clear=True) + + if not events: + return + + logger.info("replaying_buffered_events", extra={ + "run_id": run_id, + "event_count": len(events) + }) + + # Send a "replay_start" marker + await event_manager.emit_system_event( + "replay_start", + {"run_id": run_id, "event_count": len(events)} + ) + + # Replay each event + for event in events: + try: + await event_manager.emit_raw(event.event_type, event.event_data) + except Exception as e: + logger.error("event_replay_failed", extra={ + "run_id": run_id, + "event_type": event.event_type, + "error": str(e) + }) + + # Send a "replay_end" marker + await event_manager.emit_system_event( + "replay_end", + {"run_id": run_id, "events_replayed": len(events)} + ) + + def terminate_run(self, run_id: str) -> List[asyncio.Task]: + """ + Terminate a run and return its tasks for cancellation. + + This should be called when: + - Grace period expires + - User explicitly stops the run + - Run completes normally + """ + run_state = self._run_states.pop(run_id, None) + self._run_to_session.pop(run_id, None) + + if not run_state: + return [] + + run_state.state = ConnectionState.TERMINATED + tasks = list(run_state.tasks.values()) + + logger.info("run_terminated", extra={ + "run_id": run_id, + "session_id": run_state.session_id, + "task_count": len(tasks) + }) + + return tasks + + # ------------------------------------------------------------------------- + # Heartbeat Management + # ------------------------------------------------------------------------- + + async def start_heartbeat(self, session_id: str): + """Start heartbeat monitoring for a session.""" + heartbeat_state = self._heartbeat_states.get(session_id) + if not heartbeat_state: + return + + # Cancel existing heartbeat task if any + if heartbeat_state.heartbeat_task and not heartbeat_state.heartbeat_task.done(): + heartbeat_state.heartbeat_task.cancel() + + heartbeat_state.heartbeat_task = asyncio.create_task( + self._heartbeat_loop(session_id) + ) + + logger.debug("heartbeat_started", extra={"session_id": session_id}) + + async def _heartbeat_loop(self, session_id: str): + """Heartbeat loop that sends pings and monitors pongs.""" + try: + while True: + await asyncio.sleep(ConnectionConfig.HEARTBEAT_INTERVAL_SECONDS) + + websocket = self._websockets.get(session_id) + heartbeat_state = self._heartbeat_states.get(session_id) + + if not websocket or not heartbeat_state: + break + + try: + # Send ping + await websocket.send_json({ + "type": "ping", + "timestamp": datetime.now().isoformat() + }) + heartbeat_state.record_ping() + + logger.debug("heartbeat_ping_sent", extra={"session_id": session_id}) + + except Exception as e: + # Connection likely dead + missed = heartbeat_state.record_missed() + logger.warning("heartbeat_ping_failed", extra={ + "session_id": session_id, + "missed_count": missed, + "error": str(e) + }) + + if missed >= ConnectionConfig.MAX_MISSED_HEARTBEATS: + logger.warning("heartbeat_max_missed", extra={ + "session_id": session_id, + "missed_count": missed + }) + # Trigger disconnection handling + self.unregister_connection(session_id) + break + + except asyncio.CancelledError: + logger.debug("heartbeat_cancelled", extra={"session_id": session_id}) + except Exception as e: + logger.error("heartbeat_loop_error", extra={ + "session_id": session_id, + "error": str(e) + }) + + def handle_pong(self, session_id: str): + """Handle a pong response from client.""" + heartbeat_state = self._heartbeat_states.get(session_id) + if heartbeat_state: + heartbeat_state.record_pong() + logger.debug("heartbeat_pong_received", extra={"session_id": session_id}) + + async def handle_client_heartbeat( + self, + session_id: str, + client_timestamp: Optional[float] = None, + run_id: Optional[str] = None, + ): + """ + Handle client-initiated heartbeat. + + This complements the server-initiated ping/pong by allowing the client + to verify server responsiveness. Useful for detecting scenarios where: + - Server process is overloaded + - Server is alive but not processing requests + + Args: + session_id: The session sending the heartbeat + client_timestamp: Timestamp from client (for latency calculation) + run_id: Optional run ID for run-specific tracking + """ + heartbeat_state = self._heartbeat_states.get(session_id) + + if heartbeat_state: + # Track client heartbeat timing (use last_pong_received for simplicity) + heartbeat_state.record_pong() + + # Calculate latency if timestamp provided + latency_ms = None + if client_timestamp: + try: + now_ms = time.time() * 1000 + latency_ms = now_ms - client_timestamp + except (TypeError, ValueError): + pass + + logger.debug( + "client_heartbeat_received", + extra={ + "session_id": session_id, + "run_id": run_id, + "latency_ms": latency_ms, + } + ) + + # If run_id provided, touch the run state to show activity + if run_id: + run_state = self._run_states.get(run_id) + if run_state and run_state.session_id == session_id: + # Update activity timestamp (could extend grace period if in grace) + # For now, just log that run is still being monitored + logger.debug( + "client_heartbeat_run_activity", + extra={"run_id": run_id, "session_id": session_id} + ) + + # ------------------------------------------------------------------------- + # Event Buffering + # ------------------------------------------------------------------------- + + def should_buffer_event(self, run_id: str) -> bool: + """Check if events should be buffered (connection in grace period).""" + run_state = self._run_states.get(run_id) + if not run_state: + return False + return run_state.state == ConnectionState.GRACE_PERIOD + + def buffer_event(self, run_id: str, event_type: str, event_data: Dict[str, Any]): + """Buffer an event for later replay.""" + run_state = self._run_states.get(run_id) + if run_state: + run_state.buffer_event(event_type, event_data) + logger.debug("event_buffered", extra={ + "run_id": run_id, + "event_type": event_type, + "buffer_size": len(run_state.event_buffer) + }) + + # ------------------------------------------------------------------------- + # Checkpointing + # ------------------------------------------------------------------------- + + def save_checkpoint(self, run_id: str, checkpoint_data: Dict[str, Any]): + """Save a checkpoint for a run.""" + run_state = self._run_states.get(run_id) + if run_state: + run_state.save_checkpoint(checkpoint_data) + + def get_checkpoint(self, run_id: str) -> Optional[Dict[str, Any]]: + """Get the last checkpoint for a run.""" + run_state = self._run_states.get(run_id) + if run_state: + return run_state.checkpoint_data + return None + + # ------------------------------------------------------------------------- + # Grace Period Monitor + # ------------------------------------------------------------------------- + + def _ensure_monitor_running(self): + """Ensure the grace period monitor is running.""" + if self._monitor_task is None or self._monitor_task.done(): + self._monitor_task = asyncio.create_task(self._grace_period_monitor()) + + async def _grace_period_monitor(self): + """Monitor runs in grace period and terminate expired ones.""" + try: + while True: + await asyncio.sleep(5) # Check every 5 seconds + + expired_runs = [] + for run_id, run_state in list(self._run_states.items()): + if run_state.state == ConnectionState.GRACE_PERIOD: + if run_state.is_grace_period_expired(): + expired_runs.append(run_id) + + for run_id in expired_runs: + logger.warning("grace_period_expired", extra={"run_id": run_id}) + tasks = self.terminate_run(run_id) + + # Cancel all tasks + for task in tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as e: + logger.error("task_cancellation_error", extra={ + "run_id": run_id, + "error": str(e) + }) + + # Stop monitor if no runs in grace period + if not any( + rs.state == ConnectionState.GRACE_PERIOD + for rs in self._run_states.values() + ): + break + + except asyncio.CancelledError: + pass + except Exception as e: + logger.error("grace_period_monitor_error", extra={"error": str(e)}) + + # ------------------------------------------------------------------------- + # Task Management + # ------------------------------------------------------------------------- + + def register_task(self, run_id: str, task_name: str, task: asyncio.Task): + """Register a task for a run.""" + run_state = self._run_states.get(run_id) + if run_state: + run_state.tasks[task_name] = task + + def unregister_task(self, run_id: str, task_name: str): + """Unregister a task from a run.""" + run_state = self._run_states.get(run_id) + if run_state: + run_state.tasks.pop(task_name, None) + + # ------------------------------------------------------------------------- + # Utility Methods + # ------------------------------------------------------------------------- + + def get_session_for_run(self, run_id: str) -> Optional[str]: + """Get the session_id for a run.""" + return self._run_to_session.get(run_id) + + def get_event_manager_for_run(self, run_id: str) -> Optional['SessionEventManager']: + """Get the event manager for a run.""" + session_id = self._run_to_session.get(run_id) + if session_id: + return self._event_managers.get(session_id) + return None + + def get_websocket_for_run(self, run_id: str) -> Optional['WebSocket']: + """Get the websocket for a run.""" + session_id = self._run_to_session.get(run_id) + if session_id: + return self._websockets.get(session_id) + return None + + def get_runs_in_grace_period(self) -> List[str]: + """Get all run IDs currently in grace period.""" + return [ + run_id for run_id, state in self._run_states.items() + if state.state == ConnectionState.GRACE_PERIOD + ] + + def get_active_run_ids(self) -> List[str]: + """Get all active run IDs (connected or in grace period).""" + return [ + run_id for run_id, state in self._run_states.items() + if state.state in (ConnectionState.CONNECTED, ConnectionState.GRACE_PERIOD) + ] + + def get_stats(self) -> Dict[str, Any]: + """Get connection manager statistics.""" + return { + "total_runs": len(self._run_states), + "connected_runs": sum( + 1 for rs in self._run_states.values() + if rs.state == ConnectionState.CONNECTED + ), + "grace_period_runs": sum( + 1 for rs in self._run_states.values() + if rs.state == ConnectionState.GRACE_PERIOD + ), + "active_websockets": len(self._websockets), + "active_heartbeats": len(self._heartbeat_states) + } + + +# Global instance +connection_manager = ConnectionManager() diff --git a/core/api/events.py b/core/api/events.py index 4377e94..d5847f7 100644 --- a/core/api/events.py +++ b/core/api/events.py @@ -11,6 +11,17 @@ from .session import active_runs_store, active_event_managers # Import global stores logger = logging.getLogger(__name__) +# Forward reference to avoid circular import +_connection_manager = None + +def _get_connection_manager(): + """Lazy import of connection manager to avoid circular imports.""" + global _connection_manager + if _connection_manager is None: + from .connection_manager import connection_manager + _connection_manager = connection_manager + return _connection_manager + class SessionEventManager: """Session Event Manager @@ -20,7 +31,7 @@ class SessionEventManager: 3. Error handling 4. MCP tool calls """ - + def __init__(self, session_id: str): """Initializes the event manager @@ -33,7 +44,7 @@ def __init__(self, session_id: str): self.on_send: Optional[callable] = None # session_id is now the connection credential ID for the WebSocket, mainly used for logging logger.debug("event_manager_created", extra={"session_id": session_id}) - + def attach(self, on_send): self.on_send = on_send @@ -46,7 +57,7 @@ def connect(self, websocket: WebSocket): self.websocket = websocket self.is_connected = True logger.info("websocket_connection_established", extra={"session_id": self.session_id}) - + async def disconnect(self): """Marks the WebSocket connection as disconnected and tries to cancel associated long-running tasks.""" original_websocket = self.websocket @@ -57,7 +68,7 @@ async def disconnect(self): # Removed the old task cancellation logic based on top_level_shared. # Task cancellation is now handled by the finally block of the websocket_endpoint in api/server.py, # based on websocket.state.active_run_tasks. - + # Ensure the original websocket connection object is properly closed (if not handled automatically by FastAPI) # Usually FastAPI handles the closing, but call it explicitly to be sure if original_websocket: @@ -70,21 +81,42 @@ async def disconnect(self): logger.warning("websocket_close_error", extra={"session_id": self.session_id, "error": str(e)}, exc_info=True) - async def _send(self, message: Dict): - """Internal send method, responsible for checking the connection, JSON serialization, and the actual send operation + async def _send(self, message: Dict, run_id: Optional[str] = None): + """Internal send method with event buffering support. + + If connection is lost but run is in grace period, events are buffered + for replay on reconnection. Args: message: The message dictionary to send + run_id: Optional run_id for buffering context """ + # Extract run_id from message if not provided + if run_id is None: + run_id = message.get('run_id') + if not self.is_connected or not self.websocket: + # Check if we should buffer this event + if run_id: + conn_manager = _get_connection_manager() + if conn_manager.should_buffer_event(run_id): + # Buffer the event for replay on reconnection + conn_manager.buffer_event(run_id, message.get('type', 'unknown'), message) + logger.debug("event_buffered_for_reconnection", extra={ + "session_id": self.session_id, + "run_id": run_id, + "message_type": message.get('type', 'unknown') + }) + return + logger.debug("websocket_not_connected_message_dropped", extra={"session_id": self.session_id, "message_type": message.get('type', 'unknown')}) return - + try: # Add session ID if "session_id" not in message: message["session_id"] = self.session_id - + # Manually serialize JSON and send as text # Use default=str to handle objects that cannot be directly serialized, converting them to string form message_json = json.dumps(message, ensure_ascii=False, default=str) @@ -100,16 +132,27 @@ async def _send(self, message: Dict): "WebSocket is not connected" in str(e) or \ "Cannot call send" in str(e): # For starlette.websockets.WebSocketException logger.warning("websocket_send_failed_connection_closing", extra={"session_id": self.session_id, "message_type": message.get('type', 'unknown'), "error": str(e)}) - self.is_connected = False - self.websocket = None + self.is_connected = False + self.websocket = None + + # Try to buffer the event if in grace period + if run_id: + conn_manager = _get_connection_manager() + if conn_manager.should_buffer_event(run_id): + conn_manager.buffer_event(run_id, message.get('type', 'unknown'), message) + logger.debug("event_buffered_after_send_failure", extra={ + "session_id": self.session_id, + "run_id": run_id, + "message_type": message.get('type', 'unknown') + }) else: # Other RuntimeErrors logger.error("message_send_failed_runtime_error", extra={"session_id": self.session_id, "message_type": message.get('type', 'unknown'), "error": str(e)}, exc_info=True) except Exception as e: # All other exceptions logger.error("message_send_failed_general", extra={"session_id": self.session_id, "message_type": message.get('type', 'unknown'), "error": str(e)}, exc_info=True) - + async def emit_llm_chunk(self, run_id: str, agent_id: str, parent_agent_id: Optional[str], chunk_type: str, content: str, stream_id: Optional[str] = None, is_first_chunk: bool = False, is_completion_marker: bool = False, llm_id: Optional[str] = None, contextual_data: Optional[Dict] = None): """Sends an LLM streaming output chunk - + Args: run_id: The run ID agent_id: The agent ID @@ -140,7 +183,7 @@ async def emit_llm_chunk(self, run_id: str, agent_id: str, parent_agent_id: Opti "data": message_data } await self._send(message) - + async def emit_llm_response(self, run_id: str, agent_id: str, parent_agent_id: Optional[str], content: Optional[str], tool_calls: Optional[List[Dict]], reasoning: Optional[str] = None, stream_id: Optional[str] = None, llm_id: Optional[str] = None, contextual_data: Optional[Dict] = None): """Sends a complete LLM response @@ -159,9 +202,9 @@ async def emit_llm_response(self, run_id: str, agent_id: str, parent_agent_id: O content_summary = content[:50] + "..." if content and len(content) > 50 else content tool_calls_summary = f"{len(tool_calls)} tool calls" if tool_calls else "No tool calls" has_reasoning = "Yes" if reasoning else "No" - + logger.debug("llm_response_generated", extra={"run_id": run_id, "agent_id": agent_id, "content_summary": content_summary, "tool_calls_summary": tool_calls_summary, "has_reasoning": has_reasoning}) - + message_data = { "content": content, "tool_calls": tool_calls, @@ -181,11 +224,11 @@ async def emit_llm_response(self, run_id: str, agent_id: str, parent_agent_id: O "data": message_data } await self._send(message) - + # DEPRECATED: emit_agent_status has been removed # Agent status is now tracked through the Turn model in team_state # Use turn status ('running', 'completed', 'error') instead - + async def emit_resource(self, run_id: str, agent_id: str, resource_type: str, resource_data: Any, contextual_data: Optional[Dict] = None): """Sends resource data @@ -205,14 +248,14 @@ async def emit_resource(self, run_id: str, agent_id: str, resource_type: str, re resource_summary = f"Length: {len(resource_data)}" else: resource_summary = f"Type: {type(resource_data).__name__}" - + logger.debug("resource_emitted", extra={"run_id": run_id, "agent_id": agent_id, "resource_type": resource_type, "resource_summary": resource_summary}) - + # resource_data is the primary content for the 'data' field of a resource event. # If contextual_data is provided, it should be merged into this. # However, the typical use of 'data' in 'resource' event is the resource_data itself. # Let's clarify: if resource_data is a dict, merge. Otherwise, wrap. - + final_data_payload = {} if isinstance(resource_data, dict): final_data_payload.update(resource_data) @@ -230,11 +273,11 @@ async def emit_resource(self, run_id: str, agent_id: str, resource_type: str, re "data": final_data_payload } await self._send(message) - + # DEPRECATED: emit_state/state_sync has been removed # State synchronization is now handled through turns_sync and view model updates # Use emit_turns_sync() instead for state updates - + async def emit_error(self, run_id: Optional[str], agent_id: Optional[str], error_message: str, contextual_data: Optional[Dict] = None): """Sends an error message @@ -245,7 +288,7 @@ async def emit_error(self, run_id: Optional[str], agent_id: Optional[str], error contextual_data: Additional context data, which will be merged into the data field of the event """ logger.error("error_event_emitted", extra={"run_id": run_id or 'N/A', "agent_id": agent_id or 'System', "error_message": error_message}) - + message_data = { "message": error_message } @@ -295,7 +338,7 @@ async def emit_llm_stream_failed(self, run_id: str, agent_id: str, parent_agent_ } if contextual_data: message_data.update(contextual_data) - + message = { "type": "llm_stream_failed", "run_id": run_id, @@ -350,7 +393,7 @@ def _filter_credentials(self, params: Dict) -> Dict: if keyword in key_lower: filtered_params[key] = "[REDACTED]" break # Move to the next key once a keyword is found - + # Recursively filter if the value is a dictionary if isinstance(filtered_params[key], dict): filtered_params[key] = self._filter_credentials(filtered_params[key]) @@ -372,6 +415,22 @@ async def emit_run_ready(self, run_id: str, request_id: str): } }) + async def emit_run_stopped(self, run_id: str, reason: str = "user_requested"): + """Sends a run_stopped event to notify the client that a run has been stopped. + + Args: + run_id: The ID of the run that was stopped + reason: The reason for stopping (e.g., "user_requested", "error", "completed") + """ + logger.info("run_stopped_emitted", extra={"run_id": run_id, "reason": reason}) + await self._send({ + "type": "run_stopped", + "data": { + "run_id": run_id, + "reason": reason + } + }) + # DEPRECATED: emit_tool_result has been removed # Tool results are now handled through TOOL_RESULT inbox items in AgentNode # Tool interactions are tracked in the Turn model's tool_interactions array @@ -387,7 +446,7 @@ async def emit_run_config_updated(self, run_id: str, config_type: str, item_iden contextual_data: Additional context data """ logger.info("run_config_updated", extra={"run_id": run_id, "config_type": config_type, "item_identifier": item_identifier or 'N/A'}) - + message_data = { "config_type": config_type, "item_identifier": item_identifier, @@ -404,16 +463,16 @@ async def emit_run_config_updated(self, run_id: str, config_type: str, item_iden await self._send(message) async def _hydrate_turn_interactions(self, turns: List[Dict], kb: Any) -> List[Dict]: - """ + """ Iterates through turns and hydrates the result_payload in tool_interactions. """ if not kb: return turns - + return await kb.hydrate_turn_list_tool_results(turns) async def emit_turns_sync(self, context: Dict[str, Any]): - """ + """ (Modified) Sends the complete list of turns to synchronize the frontend state, and hydrates before sending. """ if not context: @@ -470,7 +529,7 @@ async def emit_work_module_updated(self, run_id: str, module_data: Dict, context contextual_data: Additional context data """ logger.info("work_module_updated", extra={"run_id": run_id, "module_id": module_data.get('module_id'), "status": module_data.get('status')}) - + message_data = { "module": module_data } @@ -496,7 +555,7 @@ async def send_json(self, run_id: Optional[str], message: Dict, contextual_data: # Add run_id to the message if it doesn't exist if "run_id" not in message and run_id is not None: message["run_id"] = run_id - + if contextual_data and "data" in message and isinstance(message["data"], dict): message["data"].update(contextual_data) elif contextual_data and "data" not in message: # If no data field, but contextual_data exists, add it as data @@ -518,6 +577,42 @@ async def emit_turn_completed(self, run_id: str, turn_id: str, agent_id: str): } await self._send(message) + async def emit_system_event(self, event_type: str, data: Dict[str, Any]): + """Sends a system-level event (not tied to a specific run). + + Used for connection management events like replay_start, replay_end, etc. + + Args: + event_type: The type of system event + data: The event data + """ + message = { + "type": event_type, + "data": data + } + await self._send(message) + logger.debug("system_event_sent", extra={"event_type": event_type}) + + async def emit_raw(self, event_type: str, event_data: Dict[str, Any]): + """Sends a raw event message directly. + + Used primarily for replaying buffered events during reconnection. + + Args: + event_type: The event type + event_data: The complete event data dictionary + """ + # If event_data already has the full message structure, use it directly + if "type" in event_data: + await self._send(event_data) + else: + # Otherwise wrap it + message = { + "type": event_type, + "data": event_data + } + await self._send(message) + async def broadcast_project_structure_update(reason: str, details: Dict[str, Any]): """ Broadcasts a project structure updated event to all active WebSocket sessions. diff --git a/core/api/message_handlers.py b/core/api/message_handlers.py index 1aed78a..f480cc1 100644 --- a/core/api/message_handlers.py +++ b/core/api/message_handlers.py @@ -24,6 +24,8 @@ from agent_core.events.event_triggers import trigger_view_model_update # For view model updates from agent_core.nodes.custom_nodes.stage_planner_node import _apply_work_module_actions # For direct work module management from agent_core.utils.serialization import get_serializable_run_snapshot # New import +# Import connection manager for resilient connection handling +from api.connection_manager import connection_manager logger = logging.getLogger(__name__) @@ -63,7 +65,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat if not action or not profile_logical_name: logger.warning("profile_update_missing_required_fields", extra={"update_request": update_request, "has_action": bool(action), "has_profile_logical_name": bool(profile_logical_name)}) continue - + logger.info("profile_update_processing_started", extra={"run_id": run_id, "action": action, "profile_logical_name": profile_logical_name}) base_profile_dict: Optional[Dict] = None @@ -118,7 +120,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat if not original_profile_for_event: logger.error("update_action_profile_not_found", extra={"profile_logical_name": profile_logical_name}, exc_info=True) continue - + new_profile_instance = copy.deepcopy(original_profile_for_event) new_profile_instance["profile_id"] = str(uuid.uuid4()) new_profile_instance["rev"] = original_profile_for_event.get("rev", 0) + 1 @@ -131,7 +133,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat if prof.get("name") == profile_logical_name and prof.get("is_active") is True: profiles_store[inst_id]["is_active"] = False logger.info("update_action_deactivated_old_version", extra={"profile_name": prof.get('name'), "instance_id": inst_id, "revision": prof.get('rev')}) - + # --- Action: DISABLE --- elif action == "DISABLE": disabled_count = 0 @@ -152,7 +154,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat if not new_logical_name_for_rename: logger.error("rename_action_missing_new_name", extra={"profile_logical_name": profile_logical_name}, exc_info=True) continue - + original_profile_for_event = get_active_profile_by_name(profiles_store, profile_logical_name) # This is the "old" profile if not original_profile_for_event: logger.error("rename_action_original_not_found", extra={"profile_logical_name": profile_logical_name}, exc_info=True) @@ -177,7 +179,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat if prof.get("name") == profile_logical_name and prof.get("is_active") is True: # Check original name profiles_store[inst_id]["is_active"] = False logger.info("rename_action_deactivated_old_profile", extra={"profile_name": prof.get('name'), "instance_id": inst_id, "revision": prof.get('rev')}) - + else: # Unknown action logger.warning("unknown_profile_action", extra={"action": action, "profile_logical_name": profile_logical_name}) continue @@ -232,7 +234,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat else: # Should not happen if logic is correct logger.warning("profile_update_no_instance_created", extra={"action": action, "run_id": run_id}) continue - + # Emit event if event_manager: if run_context['meta'].get("run_type") == "partner_interaction": @@ -243,7 +245,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat # Remove the old flag-based notification # if "flags" not in partner_state: partner_state["flags"] = {} # partner_state["flags"]["available_profiles_updated"] = True - + # Add an InboxItem instead partner_state.setdefault("inbox", []).append({ "item_id": f"inbox_{uuid.uuid4().hex[:8]}", @@ -254,7 +256,7 @@ async def _apply_profile_updates_in_run_context(run_context: Dict, profile_updat }) logger.info("profile_update_notification_added", extra={"run_id": run_id, "context_type": "partner", "action": action}) # --- End Inbox Migration --- - + await event_manager.emit_run_config_updated( run_id=run_id, config_type="agent_profile", @@ -282,6 +284,28 @@ async def handle_start_run_message(ws_state: Dict, data: Dict): logger.warning("resume_missing_request_id", extra={"session_id": session_id_for_log, "data": data}) await event_manager.emit_error(run_id=resume_from_run_id, agent_id="System", error_message="Command 'start_run' for resume requires 'request_id'.") return + + # --- Check if run is already in memory (e.g., stopped but not terminated) --- + existing_context = active_runs_store.get(resume_from_run_id) + if existing_context: + logger.info("resume_from_memory", extra={"run_id": resume_from_run_id, "current_status": existing_context.get('meta', {}).get('status')}) + + # Update the event manager reference for the new WebSocket connection + existing_context['runtime']['event_manager'] = event_manager + + # Ensure status is AWAITING_INPUT + existing_context['meta']['status'] = 'AWAITING_INPUT' + + # Register with connection manager for the new session + connection_manager.register_run(resume_from_run_id, event_manager.session_id) + + await event_manager.emit_run_ready(resume_from_run_id, request_id) + await event_manager.emit_turns_sync(existing_context) + + logger.info("resume_from_memory_completed", extra={"run_id": resume_from_run_id}) + return + + # --- Run not in memory, load from disk --- try: iic_path_str = await find_iic_file_by_run_id(resume_from_run_id) if not iic_path_str: @@ -294,7 +318,7 @@ async def handle_start_run_message(ws_state: Dict, data: Dict): json_path = iic_path_obj.parent / f"{resume_from_run_id}.json" if not json_path.exists(): raise FileNotFoundError(f"State file {json_path} not found for run_id {resume_from_run_id}") - + with open(json_path, 'r', encoding='utf-8') as f: restored_state_data = json.load(f) logger.info("resume_state_loaded", extra={"run_id": resume_from_run_id, "json_path": str(json_path)}) @@ -323,18 +347,22 @@ async def handle_start_run_message(ws_state: Dict, data: Dict): logger.info("resume_status_set", extra={"run_id": resume_from_run_id, "status": "AWAITING_INPUT"}) active_runs_store[server_run_id] = run_context - + + # Register resumed run with connection manager for resilient handling + connection_manager.register_run(server_run_id, event_manager.session_id) + await event_manager.emit_run_ready(server_run_id, request_id) - + # Send turns_sync to provide the authoritative data for rendering the conversation. await event_manager.emit_turns_sync(run_context) - + logger.info("resume_completed", extra={"run_id": server_run_id}) - + if run_context['meta']['run_type'] == "partner_interaction": partner_ctx = run_context['sub_context_refs']['_partner_context_ref'] task = asyncio.create_task(run_partner_interaction_async(partner_context=partner_ctx)) - ws_state.active_run_tasks[server_run_id] = task + run_context['runtime']['active_task'] = task # Store in run_context + ws_state.active_run_tasks[server_run_id] = task # Also track for cleanup logger.info("resume_partner_task_started", extra={"run_id": server_run_id, "run_type": "partner_interaction"}) except Exception as e: @@ -367,10 +395,13 @@ async def handle_start_run_message(ws_state: Dict, data: Dict): ) if initial_filename: run_context['meta']['initial_filename'] = initial_filename - + logger.info("new_run_context_created", extra={"run_id": server_run_id, "status": "CREATED"}) active_runs_store[server_run_id] = run_context + # Register run with connection manager for resilient handling + connection_manager.register_run(server_run_id, event_manager.session_id) + # --- NEW: Perform initial persistence BEFORE sending run_ready --- from agent_core.iic.core.iic_handlers import persist_initial_run_state await persist_initial_run_state(run_context) @@ -386,10 +417,15 @@ async def handle_start_run_message(ws_state: Dict, data: Dict): async def handle_stop_run_message(ws_state: Dict, data: Dict): - """Handles messages of type 'stop_run'""" + """Handles messages of type 'stop_run'. + + This stops the current execution task but KEEPS the run context in memory. + The user can send a new message to continue the conversation immediately + without needing to resume from disk. + """ run_id_to_stop = data.get("run_id") - active_runs_tasks = ws_state.active_run_tasks # Changed: Using HEAD's way - event_manager = ws_state.event_manager # Changed: Using HEAD's way + active_runs_tasks = ws_state.active_run_tasks + event_manager = ws_state.event_manager session_id_for_log = event_manager.session_id if not run_id_to_stop: @@ -410,16 +446,24 @@ async def handle_stop_run_message(ws_state: Dict, data: Dict): logger.warning("stop_run_cancellation_timeout", extra={"run_id": run_id_to_stop, "session_id": session_id_for_log}) except Exception as e: logger.error("stop_run_await_error", extra={"run_id": run_id_to_stop, "session_id": session_id_for_log, "error_message": str(e)}, exc_info=True) - - # State updates are now handled in the flow's `except CancelledError` block - # to prevent race conditions. + + # Remove the task from tracking (but NOT the run context) + if run_id_to_stop in active_runs_tasks: + del active_runs_tasks[run_id_to_stop] else: logger.info("stop_run_no_active_task", extra={"session_id": session_id_for_log, "run_id": run_id_to_stop}) - # The 'no_active_flow_to_stop_or_already_done' status is now inferred on the client. - - if run_id_to_stop in active_runs_store: - del active_runs_store[run_id_to_stop] - logger.info("stop_run_context_removed", extra={"session_id": session_id_for_log, "run_id": run_id_to_stop}) + + # Update run status to AWAITING_INPUT so it can receive new messages + run_context = active_runs_store.get(run_id_to_stop) + if run_context: + run_context['meta']['status'] = 'AWAITING_INPUT' + # Clear the task reference in run_context so it can be restarted + if 'active_task' in run_context.get('runtime', {}): + run_context['runtime']['active_task'] = None + logger.info("stop_run_status_updated", extra={"session_id": session_id_for_log, "run_id": run_id_to_stop, "new_status": "AWAITING_INPUT"}) + + # Notify the frontend that the run has been stopped (but is still resumable in-memory) + await event_manager.emit_run_stopped(run_id_to_stop, reason="user_requested") async def handle_request_available_toolsets(ws_state: Dict, data: Dict): @@ -427,14 +471,14 @@ async def handle_request_available_toolsets(ws_state: Dict, data: Dict): event_manager = ws_state.event_manager # Changed: Using HEAD's way session_id_for_log = event_manager.session_id logger.debug("request_available_toolsets_received", extra={"session_id": session_id_for_log, "data": data}) - - scope_filter = data.get("scope") - + + scope_filter = data.get("scope") + try: toolsets_info = get_all_toolsets_with_tools(scope_filter=scope_filter) - - await event_manager.send_json( - run_id=None, + + await event_manager.send_json( + run_id=None, message={ "type": "available_toolsets_response", "data": {"toolsets": toolsets_info} @@ -444,8 +488,8 @@ async def handle_request_available_toolsets(ws_state: Dict, data: Dict): except Exception as e: logger.error("request_available_toolsets_error", extra={"session_id": session_id_for_log, "error_message": str(e)}, exc_info=True) await event_manager.emit_error( - run_id=None, - agent_id="System", + run_id=None, + agent_id="System", error_message=f"Failed to retrieve toolsets: {str(e)}" ) @@ -489,21 +533,21 @@ async def handle_stop_managed_principal_message(ws_state: Dict, data: Dict): if principal_subtask_id and hasattr(ws_state, 'active_run_tasks') and principal_subtask_id in ws_state.active_run_tasks: del ws_state.active_run_tasks[principal_subtask_id] logger.info("stop_managed_principal_task_removed", extra={"session_id": session_id_for_log, "principal_subtask_id": principal_subtask_id}) - + # V4.1: Access runtime for these handles if run_context['runtime'].get("principal_flow_task_handle") is principal_task_handle: run_context['runtime']["principal_flow_task_handle"] = None if run_context['runtime'].get("current_principal_subtask_id") == principal_subtask_id: run_context['runtime']["current_principal_subtask_id"] = None - + partner_context_ref = run_context['sub_context_refs'].get("_partner_context_ref") # V4.1: Access sub_context_refs if partner_context_ref and partner_context_ref.get("state"): partner_context_ref["state"]["is_principal_flow_running"] = False logger.info("stop_managed_principal_flag_updated", extra={"session_id": session_id_for_log, "managing_partner_run_id": managing_partner_run_id, "is_principal_flow_running": False}) - + # The 'principal_task_stopped_by_request' status is now inferred on the client from the turn status. logger.info("stop_managed_principal_completed", extra={"managing_partner_run_id": managing_partner_run_id}) - + elif principal_task_handle and principal_task_handle.done(): logger.info("stop_managed_principal_already_done", extra={"session_id": session_id_for_log, "principal_subtask_id": principal_subtask_id, "managing_partner_run_id": managing_partner_run_id}) # The 'principal_task_already_done' status is now inferred on the client. @@ -525,14 +569,14 @@ async def handle_request_run_profiles_message(ws_state: Dict, data: Dict): """Handles 'request_run_profiles' messages, returning the Agent Profile information for the specified run.""" event_manager = ws_state.event_manager # Changed: Using HEAD's way session_id_for_log = event_manager.session_id - + run_id = data.get("run_id") logger.info("request_run_profiles_received", extra={"session_id": session_id_for_log, "run_id": run_id, "data": data}) if not run_id: logger.warning("request_run_profiles_missing_run_id", extra={"session_id": session_id_for_log}) await event_manager.send_json( - run_id=None, + run_id=None, message={ "type": "run_profiles_response", "run_id": run_id, @@ -617,7 +661,7 @@ async def handle_request_run_context_message(ws_state: Dict, data: Dict): # sanitized_context = sanitize_context_for_serialization(run_context) # Old call snapshot_context = get_serializable_run_snapshot(run_context) # New call logger.debug("run_context_snapshot_completed", extra={"session_id": session_id_for_log, "run_id": run_id}) - + await event_manager.send_json( run_id=run_id, message={ @@ -642,7 +686,7 @@ async def handle_request_knowledge_base_message(ws_state: Dict, data: Dict): """Handles 'request_knowledge_base' messages, returning the knowledge base content for the specified run.""" event_manager = ws_state.event_manager session_id_for_log = event_manager.session_id - + run_id = data.get("run_id") logger.info("request_knowledge_base_received", extra={"session_id": session_id_for_log, "run_id": run_id, "data": data}) @@ -671,7 +715,7 @@ async def handle_request_knowledge_base_message(ws_state: Dict, data: Dict): } ) return - + knowledge_base_instance = run_context['runtime'].get("knowledge_base") if not knowledge_base_instance: logger.warning("knowledge_base_not_found", extra={"session_id": session_id_for_log, "run_id": run_id}) @@ -688,7 +732,7 @@ async def handle_request_knowledge_base_message(ws_state: Dict, data: Dict): try: # Mainly send items_by_id, as others are indexes or internal state # Ensure the content is serializable - + # Create a serializable version of items_by_id serializable_items_by_id = {} if hasattr(knowledge_base_instance, 'items_by_id') and isinstance(knowledge_base_instance.items_by_id, dict): @@ -716,7 +760,7 @@ async def handle_request_knowledge_base_message(ws_state: Dict, data: Dict): # "items_by_uri_count": len(knowledge_base_instance.items_by_uri), # "items_by_hash_count": len(knowledge_base_instance.items_by_hash), } - + await event_manager.send_json( run_id=run_id, message={ @@ -741,10 +785,10 @@ async def handle_subscribe_to_view(ws_state: Dict, data: Dict): """Handles client requests to subscribe to a view model""" event_manager = ws_state.event_manager session_id_for_log = event_manager.session_id - + run_id = data.get("run_id") view_name = data.get("view_name") - + logger.info("subscribe_to_view_received", extra={"session_id": session_id_for_log, "run_id": run_id, "view_name": view_name}) if not run_id or not view_name: @@ -761,12 +805,12 @@ async def handle_subscribe_to_view(ws_state: Dict, data: Dict): # Record the subscription relationship (optional, if more complex unsubscribe logic is needed) if not hasattr(ws_state, 'subscriptions'): ws_state.subscriptions = {} - + subscriptions = ws_state.subscriptions if run_id not in subscriptions: subscriptions[run_id] = set() subscriptions[run_id].add(view_name) - + # Immediately push the latest view model once await trigger_view_model_update(run_context, view_name) @@ -775,10 +819,10 @@ async def handle_unsubscribe_from_view(ws_state: Dict, data: Dict): """Handles client requests to unsubscribe from a view model""" event_manager = ws_state.event_manager session_id_for_log = event_manager.session_id - + run_id = data.get("run_id") view_name = data.get("view_name") - + logger.info("unsubscribe_from_view_received", extra={"session_id": session_id_for_log, "run_id": run_id, "view_name": view_name}) if run_id and view_name and hasattr(ws_state, 'subscriptions'): @@ -810,7 +854,7 @@ async def handle_manage_work_modules_request(ws_state: Dict, data: Dict): logger.warning("manage_work_modules_context_not_found", extra={"session_id": session_id_for_log, "run_id": run_id}) await event_manager.emit_error(run_id=run_id, agent_id="System", error_message=f"Run '{run_id}' not found.") return - + team_state = run_context['team_state'] # V4.1: team_state is a direct key if not team_state: # Should not happen if run_context is valid logger.error("manage_work_modules_no_team_state", extra={"session_id": session_id_for_log, "run_id": run_id}, exc_info=True) @@ -822,7 +866,7 @@ async def handle_manage_work_modules_request(ws_state: Dict, data: Dict): if update_result.get("overall_status") != "failure": team_state["work_modules"] = update_result.get("final_work_modules", team_state.get("work_modules")) logger.info("work_modules_updated", extra={"run_id": run_id, "source": "direct_request"}) - + # Trigger kanban view update await trigger_view_model_update(run_context, "kanban_view") else: @@ -867,12 +911,12 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): # --- Branch 1: Activate a pending run --- if run_status == 'CREATED': logger.debug("run_activation_started", extra={"run_id": target_run_id, "run_type": run_type}) - + if prompt_content is None: raise ValueError("First message to a new run must contain a 'prompt'.") - + run_context['team_state']['question'] = prompt_content - + task = None if run_type == "partner_interaction": partner_context = run_context['sub_context_refs']['_partner_context_ref'] @@ -887,19 +931,20 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): "metadata": {"created_at": datetime.now(timezone.utc).isoformat()} } partner_state.setdefault("inbox", []).append(inbox_item) - + # 2. Start the task task = asyncio.create_task(run_partner_interaction_async(partner_context=partner_context)) else: raise ValueError(f"Run type '{run_type}' does not support activation via 'send_to_run'.") - ws_state.active_run_tasks[target_run_id] = task + run_context['runtime']['active_task'] = task # Store in run_context + ws_state.active_run_tasks[target_run_id] = task # Also track for cleanup task.add_done_callback( lambda t: logger.info("run_task_finished", extra={"run_id": target_run_id, "run_type": run_type, "session_id": session_id_for_log}) if not t.cancelled() else logger.info("run_task_cancelled", extra={"run_id": target_run_id, "run_type": run_type, "session_id": session_id_for_log}) ) - + run_context['meta']['status'] = 'AWAITING_INPUT' logger.debug("run_activation_completed", extra={"run_id": target_run_id, "status": "AWAITING_INPUT"}) @@ -930,6 +975,21 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): } partner_state.setdefault("inbox", []).append(inbox_item) + # Check if task exists and is running; if not, restart it + # Store task in run_context to prevent duplicate tasks across WebSocket connections + run_runtime = run_context['runtime'] + existing_task = run_runtime.get('active_task') + if existing_task is None or existing_task.done(): + logger.info("restarting_partner_task", extra={"run_id": target_run_id, "reason": "task_not_running"}) + task = asyncio.create_task(run_partner_interaction_async(partner_context=partner_context)) + run_runtime['active_task'] = task + ws_state.active_run_tasks[target_run_id] = task # Also track in ws_state for cleanup + task.add_done_callback( + lambda t: logger.info("run_task_finished", extra={"run_id": target_run_id, "run_type": run_type, "session_id": session_id_for_log}) + if not t.cancelled() else + logger.info("run_task_cancelled", extra={"run_id": target_run_id, "run_type": run_type, "session_id": session_id_for_log}) + ) + # Wake up the task new_input_event = partner_context['runtime_objects'].get("new_user_input_event") if new_input_event: @@ -937,7 +997,7 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): logger.info("partner_task_notified", extra={"run_id": target_run_id, "notification_method": "inbox"}) else: logger.error("partner_notification_failed", extra={"run_id": target_run_id, "reason": "new_user_input_event_not_found"}, exc_info=True) - + # --- Branch 3: Handle invalid states --- else: err_msg = f"Cannot send message to run {target_run_id} because its status is '{run_status}'." @@ -949,6 +1009,181 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): logger.error("send_to_run_processing_error", extra={"session_id": session_id_for_log, "target_run_id": target_run_id, "run_type": run_type, "error_message": str(e)}, exc_info=True) await event_manager.emit_error(run_id=target_run_id, agent_id="System", error_message=f"Error processing message for run {target_run_id}: {str(e)}") + +# --- Session Resilience Handlers --- + +async def handle_reconnect_message(ws_state, data: Dict): + """ + Handle reconnection request from a client that was previously connected. + + This message is sent when: + - Browser refreshes during an active run + - Network temporarily disconnects and reconnects + - Tab goes to background and comes back + + The client must provide: + - run_id: The run to reconnect to + - last_event_id: Last event received (for replay) + + Security: JWT validation happens before this handler is called. + """ + event_manager = ws_state.event_manager + session_id = getattr(ws_state, 'session_id', event_manager.session_id) + websocket = getattr(ws_state, 'websocket', None) + + run_id = data.get("run_id") + last_event_id = data.get("last_event_id", 0) + + if not run_id: + logger.warning("reconnect_missing_run_id", extra={"session_id": session_id}) + await event_manager.emit_raw("reconnect_error", { + "type": "reconnect_error", + "error": "Missing run_id in reconnect request", + }) + return + + logger.info( + "reconnect_request_received", + extra={ + "session_id": session_id, + "run_id": run_id, + "last_event_id": last_event_id, + } + ) + + # Check if run exists and can be reconnected + run_context = active_runs_store.get(run_id) + if not run_context: + logger.warning("reconnect_run_not_found", extra={"session_id": session_id, "run_id": run_id}) + await event_manager.emit_raw("reconnect_error", { + "type": "reconnect_error", + "error": f"Run {run_id} not found", + "run_id": run_id, + }) + return + + # Attempt reconnection via connection manager + try: + result = await connection_manager.reconnect_run( + run_id=run_id, + new_session_id=session_id, + websocket=websocket, + event_manager=event_manager, + ) + + if result["success"]: + # Update ws_state with reconnected run + ws_state.active_run_id = run_id + + # Get run status + run_status = run_context.get('meta', {}).get('status', 'unknown') + + # Send reconnection confirmation + await event_manager.emit_raw("reconnected", { + "type": "reconnected", + "run_id": run_id, + "run_status": run_status, + "buffered_events": result.get("buffered_events", []), + "events_replayed": result.get("events_replayed", 0), + "message": f"Successfully reconnected to run {run_id}", + }) + + logger.info( + "reconnect_success", + extra={ + "session_id": session_id, + "run_id": run_id, + "events_replayed": result.get("events_replayed", 0), + } + ) + else: + error_msg = result.get("error", "Unknown error during reconnection") + logger.warning( + "reconnect_failed", + extra={ + "session_id": session_id, + "run_id": run_id, + "error": error_msg, + } + ) + await event_manager.emit_raw("reconnect_error", { + "type": "reconnect_error", + "error": error_msg, + "run_id": run_id, + }) + + except Exception as e: + logger.error( + "reconnect_exception", + extra={ + "session_id": session_id, + "run_id": run_id, + "error": str(e), + }, + exc_info=True + ) + await event_manager.emit_raw("reconnect_error", { + "type": "reconnect_error", + "error": f"Reconnection failed: {str(e)}", + "run_id": run_id, + }) + + +async def handle_heartbeat_message(ws_state, data: Dict): + """ + Handle client-initiated heartbeat. + + Client sends heartbeat every CLIENT_HEARTBEAT_INTERVAL_SECONDS (default: 20s). + Server responds with heartbeat_ack to confirm session validity and server health. + + This complements server-initiated ping/pong by: + - Detecting server unresponsiveness (server alive but not processing) + - Providing client-side health check capability + - Enabling faster detection of server overload scenarios + """ + event_manager = ws_state.event_manager + session_id = getattr(ws_state, 'session_id', event_manager.session_id) + + client_timestamp = data.get("timestamp") + client_run_id = data.get("run_id") + client_session_id = data.get("session_id") + + # Validate session ID matches + session_valid = (client_session_id == session_id) if client_session_id else True + + # Update heartbeat state in connection manager + try: + await connection_manager.handle_client_heartbeat( + session_id=session_id, + client_timestamp=client_timestamp, + run_id=client_run_id, + ) + except Exception as e: + logger.warning( + "client_heartbeat_handling_error", + extra={"session_id": session_id, "error": str(e)}, + ) + + # Send acknowledgment + from datetime import datetime, timezone + server_time = datetime.now(timezone.utc).isoformat() + + await event_manager.emit_raw("heartbeat_ack", { + "type": "heartbeat_ack", + "timestamp": client_timestamp, + "serverTime": server_time, + "sessionValid": session_valid, + }) + + logger.debug( + "client_heartbeat_acknowledged", + extra={ + "session_id": session_id, + "client_timestamp": client_timestamp, + "run_id": client_run_id, + } + ) + # --- MESSAGE_HANDLERS registry (Dango's version, with adapted function names) --- MESSAGE_HANDLERS: Dict[str, callable] = { "start_run": handle_start_run_message, @@ -962,6 +1197,9 @@ async def handle_send_to_run_message(ws_state: Dict, data: Dict): "subscribe_to_view": handle_subscribe_to_view, # New view subscription handler "unsubscribe_from_view": handle_unsubscribe_from_view, # New view unsubscription handler "manage_work_modules_request": handle_manage_work_modules_request, # New module management handler + # Session resilience handlers + "reconnect": handle_reconnect_message, # Client reconnection to existing run + "heartbeat": handle_heartbeat_message, # Client-initiated heartbeat } # Ensure old handlers (if they were ever in a combined state) are not present diff --git a/core/api/server.py b/core/api/server.py index 8d8177d..6ccc658 100644 --- a/core/api/server.py +++ b/core/api/server.py @@ -3,18 +3,27 @@ import asyncio import os import shutil -from typing import Dict, List +from typing import Dict, List, Optional -from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, status, Request, Query, UploadFile, File +from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, status, Request, Query, UploadFile, File, Cookie, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, RedirectResponse +from pydantic import BaseModel from agent_core.iic.core.event_handler import EventHandler as IICEventHandler # from pydantic import BaseModel, Field # BaseModel, Field are no longer needed as SessionRequest will be removed # Remove imports for get_session, remove_session as they are part of the old session management # create_session is still needed, but its invocation will change -from .session import create_session, pending_websocket_sessions, active_runs_store, active_event_managers # <--- Modified import +from .session import ( + create_session, + pending_websocket_sessions, + active_runs_store, + active_event_managers, + initialize_session_security, + refresh_session_tokens, + validate_session_jwt, +) from api.events import SessionEventManager from agent_core.iic.core.iic_handlers import list_projects, get_project, create_project, delete_project, update_project, update_run_meta, delete_run, update_run_name, move_iic # Import server_manager startup/shutdown functions @@ -23,10 +32,15 @@ from .message_handlers import MESSAGE_HANDLERS # Import metadata related modules from .metadata import fetch_metadata, MetadataResponse +# Import connection manager for resilient WebSocket handling +from .connection_manager import connection_manager, ConnectionState # Configure logging (can be handled by run_server.py, or called here as well) logger = logging.getLogger(__name__) +# Global shutdown event to signal WebSocket connections to close gracefully +shutdown_event = asyncio.Event() + # Create FastAPI application app = FastAPI(title="PocketFlow Search Agent API", lifespan=lifespan_manager) @@ -36,8 +50,12 @@ allow_origins=[ "http://localhost:3000", "http://127.0.0.1:3000", + "http://localhost:3800", + "http://127.0.0.1:3800", "http://localhost:8000", # Add FastAPI port - "http://127.0.0.1:8000" + "http://127.0.0.1:8000", + "http://localhost:8800", + "http://127.0.0.1:8800", ], # Allow frontend development environment and FastAPI access allow_credentials=True, allow_methods=["*"], # Allow all HTTP methods @@ -53,7 +71,7 @@ async def redirect_to_webview(): """Redirect root path to the frontend application""" return RedirectResponse(url="/webview/", status_code=302) - + # Handle all requests under the /webview/ path @app.get("/webview/{file_path:path}", include_in_schema=False) async def serve_webview_files(file_path: str = ""): @@ -61,31 +79,31 @@ async def serve_webview_files(file_path: str = ""): # If the path is empty, default to returning index.html if not file_path or file_path == "/": file_path = "index.html" - + # Try to find the corresponding file full_file_path = os.path.join(frontend_path, file_path) - + # If the file exists, return it directly if os.path.isfile(full_file_path): return FileResponse(full_file_path) - + # If it's a directory, try to return index.html from within it if os.path.isdir(full_file_path): index_in_dir = os.path.join(full_file_path, "index.html") if os.path.isfile(index_in_dir): return FileResponse(index_in_dir) - + # New: If the file is not found, try adding the .html extension if not file_path.endswith('.html'): html_file_path = os.path.join(frontend_path, file_path + ".html") if os.path.isfile(html_file_path): return FileResponse(html_file_path) - + # If nothing is found, check for a 404.html error_404_path = os.path.join(frontend_path, "404.html") if os.path.isfile(error_404_path): return FileResponse(error_404_path, status_code=404) - + # If there isn't even a 404.html, return a simple 404 raise HTTPException(status_code=404, detail="Page not found") @@ -94,14 +112,20 @@ async def serve_webview_files(file_path: str = ""): @app.websocket("/ws/{session_id}") async def websocket_endpoint(websocket: WebSocket, session_id: str): - """WebSocket endpoint handler - adjusted according to the run_id refactoring plan""" - + """WebSocket endpoint handler with resilient connection management. + + Features: + - Heartbeat (ping/pong) to detect stale connections + - Reconnection grace period for temporary disconnects + - Event buffering during disconnection + """ + # 1. Verify if the session_id is valid and not in use if session_id not in pending_websocket_sessions: await websocket.close(code=status.WS_1008_POLICY_VIOLATION, reason="Invalid or expired session ID") logger.warning("websocket_connection_rejected", extra={"session_id": session_id, "reason": "invalid_expired_session"}) return - + # Remove from the pending list, marking it as used del pending_websocket_sessions[session_id] logger.debug("websocket_session_validated", extra={"session_id": session_id}) @@ -112,9 +136,12 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str): # session_id is passed to SessionEventManager mainly for logging purposes. event_manager = SessionEventManager(session_id=session_id) websocket.state.event_manager = event_manager - + websocket.state.session_id = session_id # Store session_id for message handlers + # 4. Initialize the list of tasks associated with this WebSocket connection websocket.state.active_run_tasks: Dict[str, asyncio.Task] = {} # type: ignore + websocket.state.active_run_id: Optional[str] = None # Track active run for reconnection + websocket.state.websocket = websocket # Store reference for reconnection handlers try: await websocket.accept() @@ -124,40 +151,52 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str): websocket.state.event_manager.attach(iic_eh.on_message) # Add the event_manager to the global list active_event_managers.append(event_manager) - # websocket.state.event_manager.attach(iic_eh.on_message) + + # Register connection with connection manager and start heartbeat + connection_manager.register_connection(session_id, websocket, event_manager) + await connection_manager.start_heartbeat(session_id) + logger.info("websocket_connection_accepted", extra={"session_id": session_id}) # Main message processing loop - # According to the refactoring plan, at this stage, the WebSocket connection is established, and the server enters a listening state. - # It waits for the client to send a command to start a specific business run (e.g., 'start_run') through this connection. - # The current MESSAGE_HANDLERS and related logic will be significantly adjusted in subsequent steps. - # This loop is simplified for now, preparing for the 'start_run' message processing. while True: + # Check if server is shutting down + if shutdown_event.is_set(): + logger.info("websocket_closing_due_to_shutdown", extra={"session_id": session_id}) + await websocket.close(code=status.WS_1001_GOING_AWAY, reason="Server shutting down") + break + try: - message_json_str = await websocket.receive_text() + # Use wait_for with timeout to periodically check shutdown event + message_json_str = await asyncio.wait_for( + websocket.receive_text(), + timeout=5.0 # Check for shutdown every 5 seconds + ) message = json.loads(message_json_str) message_type = message.get("type") message_data = message.get("data", {}) logger.debug("websocket_message_received", extra={"session_id": session_id, "message_type": message_type, "raw_preview": message_json_str[:200]}) + # Handle heartbeat pong responses + if message_type == "pong": + connection_manager.handle_pong(session_id) + continue + handler = MESSAGE_HANDLERS.get(message_type) if handler: # All handlers now expect (websocket.state, data) as parameters - # For handlers that need to start tasks (like start_run), they manage tasks internally and store them in websocket.state.active_run_tasks - # For stop_run, it cancels tasks from websocket.state.active_run_tasks - # Other handlers operate directly or send responses through event_manager await handler(websocket.state, message_data) else: logger.warning("websocket_unknown_message_type", extra={"session_id": session_id, "message_type": message_type}) if hasattr(websocket.state, 'event_manager') and websocket.state.event_manager: await websocket.state.event_manager.emit_error(run_id=None, agent_id="System", error_message=f"Unknown message type: {message_type}") - + except asyncio.TimeoutError: - # This block is used to periodically check completed tasks, similar to previous logic, but now operates on websocket.state.active_run_tasks + # This block is used to periodically check completed tasks done_run_ids = [] active_tasks_loop = websocket.state.active_run_tasks if hasattr(websocket.state, 'active_run_tasks') else {} - + if not active_tasks_loop: continue @@ -176,7 +215,7 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str): logger.info("run_task_cancelled", extra={"run_id": run_id_iter, "session_id": session_id, "reason": "timeout_loop"}) except Exception as e_check: logger.error("run_task_completion_check_error", extra={"run_id": run_id_iter, "session_id": session_id, "error": str(e_check)}, exc_info=True) - + for run_id_to_remove in done_run_ids: if run_id_to_remove in active_tasks_loop: del active_tasks_loop[run_id_to_remove] @@ -191,68 +230,232 @@ async def websocket_endpoint(websocket: WebSocket, session_id: str): except WebSocketDisconnect: logger.info("websocket_disconnected", extra={"session_id": session_id}) - break + break except json.JSONDecodeError as json_err: logger.error("websocket_invalid_json", extra={"session_id": session_id, "raw_message": message_json_str, "error": str(json_err)}) if hasattr(websocket.state, 'event_manager') and websocket.state.event_manager: await websocket.state.event_manager.emit_error(run_id=None, agent_id="System", error_message="Invalid JSON message received.") - - except Exception as e: + + except Exception as e: logger.error("websocket_unexpected_error", extra={"session_id": session_id, "error": str(e)}, exc_info=True) if hasattr(websocket.state, 'event_manager') and websocket.state.event_manager: await websocket.state.event_manager.emit_error(run_id=None, agent_id="System", error_message=f"Unexpected WebSocket processing error: {e}") - break + break finally: + # Use connection manager to handle disconnection with grace period + # This will NOT immediately cancel tasks - they continue running during grace period + runs_in_grace = connection_manager.unregister_connection(session_id) + active_tasks_at_close = websocket.state.active_run_tasks if hasattr(websocket.state, 'active_run_tasks') else {} - logger.info("websocket_cleanup_active_runs", extra={"session_id": session_id, "active_run_count": len(active_tasks_at_close)}) - - for run_id_final, task_final in list(active_tasks_at_close.items()): - if not task_final.done(): - logger.info("websocket_cancelling_run_task", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup"}) - task_final.cancel() - try: - await task_final - except asyncio.CancelledError: - logger.info("run_task_cancelled_success", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup"}) - except Exception as e_final_cancel: - logger.error("run_task_cancellation_error", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup", "error": str(e_final_cancel)}, exc_info=True) - - # Clean up run_context from global store for this run_id - if run_id_final in active_runs_store: - del active_runs_store[run_id_final] - logger.info("run_context_removed", extra={"session_id": session_id, "run_id": run_id_final, "phase": "final_cleanup"}) - else: - logger.warning("run_context_not_found", extra={"session_id": session_id, "run_id": run_id_final, "phase": "final_cleanup", "possible_cause": "already_removed_by_stop_run"}) - - if hasattr(websocket.state, 'active_run_tasks'): - websocket.state.active_run_tasks.clear() - + + if runs_in_grace: + # Runs are in grace period - tasks keep running, events will be buffered + logger.info("websocket_disconnect_grace_period_started", extra={ + "session_id": session_id, + "runs_in_grace": runs_in_grace, + "active_task_count": len(active_tasks_at_close) + }) + # Register tasks with connection manager so they can be tracked + for run_id in runs_in_grace: + run_state = connection_manager.get_run_state(run_id) + if run_state and run_id in active_tasks_at_close: + run_state.tasks[run_id] = active_tasks_at_close[run_id] + else: + # No active runs in grace period, proceed with immediate cleanup (legacy behavior) + logger.info("websocket_cleanup_active_runs", extra={"session_id": session_id, "active_run_count": len(active_tasks_at_close)}) + + for run_id_final, task_final in list(active_tasks_at_close.items()): + if not task_final.done(): + logger.info("websocket_cancelling_run_task", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup"}) + task_final.cancel() + try: + await task_final + except asyncio.CancelledError: + logger.info("run_task_cancelled_success", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup"}) + except Exception as e_final_cancel: + logger.error("run_task_cancellation_error", extra={"run_id": run_id_final, "session_id": session_id, "phase": "final_cleanup", "error": str(e_final_cancel)}, exc_info=True) + + # Clean up run_context from global store for this run_id + if run_id_final in active_runs_store: + del active_runs_store[run_id_final] + logger.info("run_context_removed", extra={"session_id": session_id, "run_id": run_id_final, "phase": "final_cleanup"}) + else: + logger.warning("run_context_not_found", extra={"session_id": session_id, "run_id": run_id_final, "phase": "final_cleanup", "possible_cause": "already_removed_by_stop_run"}) + + if hasattr(websocket.state, 'active_run_tasks'): + websocket.state.active_run_tasks.clear() + # Remove event_manager from the global list upon disconnect if event_manager in active_event_managers: active_event_managers.remove(event_manager) if hasattr(websocket.state, 'event_manager') and websocket.state.event_manager: await websocket.state.event_manager.disconnect() - - # The old remove_session(session_id, immediate=False) is no longer needed, - # because session_id is temporary and removed from pending_websocket_sessions upon connection. + logger.info("websocket_handling_finished", extra={"session_id": session_id}) + +# --- Session Security Models --- + +class SessionCreateRequest(BaseModel): + """Request body for session creation.""" + project_id: str = "default" + + +class RefreshTokenRequest(BaseModel): + """Request body for token refresh.""" + refresh_token: str + + @app.post("/session") -async def create_new_session(http_request: Request): # Removed session_request_data - """(Refactored) Creates a new temporary WebSocket connection credential (session_id). +async def create_new_session( + response: Response, + body: Optional[SessionCreateRequest] = None, + __Secure_Fgp: Optional[str] = Cookie(None, alias="__Secure-Fgp"), +): + """Creates a new secure WebSocket connection credential with JWT. + + This endpoint creates: + - A unique session_id + - A signed JWT token bound to a fingerprint cookie + - A refresh token for silent token renewal - This interface no longer accepts business-related parameters (e.g., language, saved_state). - The request body should be empty. + The fingerprint cookie is set automatically (HttpOnly, Secure, SameSite=Strict). + If an existing fingerprint cookie is present (reconnection), it will be reused. + + Returns: + session_id: Unique session identifier + jwt_token: Signed JWT for WebSocket authentication + refresh_token: Token for silent refresh before JWT expiry + expires_in: Seconds until JWT expires """ try: - result = await create_session() + project_id = body.project_id if body else "default" + result = await create_session( + response=response, + project_id=project_id, + existing_fingerprint=__Secure_Fgp, # Reuse fingerprint on reconnection + ) return result except Exception as e: logger.error("session_credential_creation_failed", extra={"error": str(e)}, exc_info=True) - # In FastAPI, it's common to raise an HTTPException or let the global exception handler handle it - return {"error": f"Failed to create session credential: {str(e)}", "status_code": 500} + raise HTTPException(status_code=500, detail=f"Failed to create session credential: {str(e)}") + + +@app.post("/session/refresh") +async def refresh_session( + body: RefreshTokenRequest, + __Secure_Fgp: Optional[str] = Cookie(None, alias="__Secure-Fgp"), +): + """Refresh session tokens using a refresh token. + + Implements token rotation: each refresh token can only be used once. + Token reuse (replaying an old refresh token) is detected and triggers + revocation of all session tokens as a security measure. + + Requires: + - refresh_token in request body + - __Secure-Fgp cookie (automatically sent by browser) + + Returns: + jwt_token: New signed JWT + refresh_token: New refresh token (old one is invalidated) + expires_in: Seconds until new JWT expires + """ + result = await refresh_session_tokens( + refresh_token=body.refresh_token, + fingerprint_cookie=__Secure_Fgp, + ) + + if not result: + raise HTTPException( + status_code=401, + detail="Invalid or expired refresh token" + ) + + return { + "jwt_token": result.jwt_token, + "refresh_token": result.refresh_token, + "expires_in": result.expires_in, + } + + +# --- Connection Resilience Endpoints --- + +@app.get("/run/{run_id}/status") +async def get_run_connection_status(run_id: str): + """Get the connection status of a run. + + Returns whether the run is active, in grace period, or terminated, + and whether it can be reconnected to. + """ + run_state = connection_manager.get_run_state(run_id) + + if not run_state: + return { + "run_id": run_id, + "exists": False, + "can_reconnect": False, + "message": "Run not found or already terminated" + } + + return { + "run_id": run_id, + "exists": True, + "state": run_state.state.value, + "can_reconnect": connection_manager.can_reconnect(run_id), + "connected_at": run_state.connected_at.isoformat() if run_state.connected_at else None, + "disconnected_at": run_state.disconnected_at.isoformat() if run_state.disconnected_at else None, + "grace_period_expires": run_state.grace_period_expires.isoformat() if run_state.grace_period_expires else None, + "buffered_events": len(run_state.event_buffer), + "last_checkpoint": run_state.last_checkpoint.isoformat() if run_state.last_checkpoint else None + } + + +@app.get("/runs/active") +async def get_active_runs(): + """Get all active runs (connected or in grace period).""" + active_run_ids = connection_manager.get_active_run_ids() + runs_info = [] + + for run_id in active_run_ids: + run_state = connection_manager.get_run_state(run_id) + if run_state: + runs_info.append({ + "run_id": run_id, + "state": run_state.state.value, + "session_id": run_state.session_id, + "can_reconnect": connection_manager.can_reconnect(run_id) + }) + + return { + "active_runs": runs_info, + "stats": connection_manager.get_stats() + } + + +@app.get("/runs/grace-period") +async def get_runs_in_grace_period(): + """Get all runs currently in reconnection grace period.""" + grace_period_runs = connection_manager.get_runs_in_grace_period() + runs_info = [] + + for run_id in grace_period_runs: + run_state = connection_manager.get_run_state(run_id) + if run_state: + runs_info.append({ + "run_id": run_id, + "session_id": run_state.session_id, + "disconnected_at": run_state.disconnected_at.isoformat() if run_state.disconnected_at else None, + "grace_period_expires": run_state.grace_period_expires.isoformat() if run_state.grace_period_expires else None, + "buffered_events": len(run_state.event_buffer) + }) + + return { + "runs_in_grace_period": runs_info, + "count": len(runs_info) + } # --- Project Management Endpoints --- @@ -282,7 +485,7 @@ async def upload_file_to_project(project_id: str, file: UploadFile = File(...)): The file monitor will automatically detect and index it. """ from agent_core.iic.core.iic_handlers import get_iic_dir - + project_path_str = get_iic_dir(project_id) if not project_path_str or not os.path.isdir(project_path_str): raise HTTPException(status_code=404, detail=f"Project with ID '{project_id}' not found.") @@ -301,14 +504,14 @@ async def upload_file_to_project(project_id: str, file: UploadFile = File(...)): raise HTTPException(status_code=400, detail="Invalid filename.") file_location = os.path.join(assets_dir, safe_filename) - + if os.path.exists(file_location): raise HTTPException(status_code=409, detail=f"File '{safe_filename}' already exists in this project's assets.") try: with open(file_location, "wb+") as file_object: shutil.copyfileobj(file.file, file_object) - + logger.info("file_upload_success", extra={"filename": safe_filename, "project_id": project_id, "file_location": file_location}) return {"info": f"File '{safe_filename}' uploaded successfully to project '{project_id}'."} except Exception as e: @@ -373,7 +576,7 @@ async def move_run_between_projects(request: Request): run_id = body.get("run_id") from_project_id = body.get("from_project_id") to_project_id = body.get("to_project_id") - + # Validate required fields if not run_id: raise HTTPException(status_code=400, detail="Missing 'run_id' in request body.") @@ -381,11 +584,11 @@ async def move_run_between_projects(request: Request): raise HTTPException(status_code=400, detail="Missing 'from_project_id' in request body.") if not to_project_id: raise HTTPException(status_code=400, detail="Missing 'to_project_id' in request body.") - + # Prevent moving to the same project if from_project_id == to_project_id: raise HTTPException(status_code=400, detail="Source and destination projects cannot be the same.") - + return move_iic(run_id, from_project_id, to_project_id) except HTTPException as e: raise e @@ -405,7 +608,7 @@ async def get_metadata(url: str = Query(..., description="The URL for which to f """ if not url: raise HTTPException(status_code=400, detail="URL is required") - + try: metadata = await fetch_metadata(url) return metadata diff --git a/core/api/session.py b/core/api/session.py index c024515..0d47b7f 100644 --- a/core/api/session.py +++ b/core/api/session.py @@ -1,10 +1,22 @@ import uuid +import os import logging -from typing import Dict, TYPE_CHECKING, List -from datetime import datetime # Ensure datetime is imported +from typing import Dict, TYPE_CHECKING, List, Optional +from datetime import datetime # Ensure datetime is imported +from fastapi import Response if TYPE_CHECKING: - from api.events import SessionEventManager # For type hinting only + from api.events import SessionEventManager # For type hinting only + +from api.session_security import ( + SessionSecurityConfig, + SessionSecurityManager, + SessionTokens, + RefreshResult, + ValidationResult, + init_security_manager, + get_security_manager, +) logger = logging.getLogger(__name__) @@ -20,31 +32,182 @@ active_event_managers: List['SessionEventManager'] = [] -async def create_session() -> dict: - """Creates a temporary WebSocket connection credential (session_id). +def initialize_session_security() -> SessionSecurityManager: + """ + Initialize the session security manager with config from environment. + + Call this during app startup. + """ + jwt_secret = os.environ.get("JWT_SECRET") + if not jwt_secret: + # Generate a random secret if not configured (development only) + logger.warning( + "JWT_SECRET not set - generating random secret. " + "This is only acceptable for development!" + ) + import secrets + jwt_secret = secrets.token_urlsafe(64) + + config = SessionSecurityConfig( + jwt_secret=jwt_secret, + jwt_expiry_minutes=int(os.environ.get("JWT_EXPIRY_MINUTES", "15")), + refresh_token_expiry_hours=int(os.environ.get("REFRESH_TOKEN_EXPIRY_HOURS", "24")), + client_heartbeat_interval_seconds=int( + os.environ.get("CLIENT_HEARTBEAT_INTERVAL_SECONDS", "20") + ), + client_heartbeat_timeout_seconds=int( + os.environ.get("CLIENT_HEARTBEAT_TIMEOUT_SECONDS", "10") + ), + client_max_missed_heartbeats=int( + os.environ.get("CLIENT_MAX_MISSED_HEARTBEATS", "2") + ), + # In development (HTTP), don't require Secure cookie + fingerprint_cookie_secure=os.environ.get("COOKIE_SECURE", "true").lower() == "true", + ) + + return init_security_manager(config) + + +async def create_session( + response: Response, + project_id: str = "default", + existing_fingerprint: Optional[str] = None, +) -> dict: + """Creates a secure WebSocket connection credential with JWT. - This function no longer initializes any business state or top_level_shared objects. - Its sole responsibility is to generate a session_id and record it for subsequent WebSocket connection request validation. + This function generates: + - A unique session_id + - A signed JWT token with fingerprint binding + - A refresh token for silent token renewal + - Sets the fingerprint HttpOnly cookie + + Args: + response: FastAPI Response object to set cookies on + project_id: Project identifier for the session + existing_fingerprint: Optional existing fingerprint for reconnection Returns: - dict: A dictionary containing the session_id and status. + dict: Session credentials including session_id, jwt_token, refresh_token """ try: session_id = str(uuid.uuid4()) + + # Create JWT and tokens via security manager + security_manager = get_security_manager() + tokens: SessionTokens = security_manager.create_session_tokens( + session_id=session_id, + project_id=project_id, + existing_fingerprint=existing_fingerprint, + ) + + # Set fingerprint cookie (HttpOnly, Secure, SameSite=Strict) + cookie_settings = security_manager.get_cookie_settings() + response.set_cookie( + value=tokens.fingerprint, + **cookie_settings, + ) + # Record this session_id and its creation timestamp pending_websocket_sessions[session_id] = datetime.now() - logger.info("websocket_credential_created", extra={"session_id": session_id}) - + logger.info( + "secure_session_created", + extra={ + "session_id": session_id, + "project_id": project_id, + "expires_in": tokens.expires_in, + } + ) + return { - "session_id": session_id, - "status": "success" + "session_id": tokens.session_id, + "jwt_token": tokens.jwt_token, + "refresh_token": tokens.refresh_token, + "expires_in": tokens.expires_in, + "status": "success", } - + + except Exception as e: + logger.error( + "secure_session_creation_failed", + extra={"error": str(e)}, + exc_info=True + ) + raise + + +async def refresh_session_tokens( + refresh_token: str, + fingerprint_cookie: Optional[str], +) -> Optional[RefreshResult]: + """ + Refresh session tokens using a refresh token. + + Args: + refresh_token: The refresh token from client + fingerprint_cookie: The fingerprint from HttpOnly cookie + + Returns: + RefreshResult with new tokens, or None if invalid + """ + try: + security_manager = get_security_manager() + result = security_manager.refresh_tokens( + refresh_token=refresh_token, + fingerprint_cookie=fingerprint_cookie, + ) + + if result: + logger.info("session_tokens_refreshed") + else: + logger.warning("session_token_refresh_failed") + + return result + except Exception as e: - logger.error("websocket_credential_creation_failed", extra={"error": str(e)}, exc_info=True) - # Consider how to throw up or return an error response in FastAPI - # For this refactoring, it is assumed that direct function calls will be handled by the caller's exception or FastAPI's error handling mechanism - raise # Or return a dictionary containing error information, depending on the API design + logger.error( + "session_token_refresh_error", + extra={"error": str(e)}, + exc_info=True + ) + return None + + +def validate_session_jwt( + jwt_token: str, + fingerprint_cookie: Optional[str], +) -> ValidationResult: + """ + Validate a session JWT token with fingerprint verification. + + Args: + jwt_token: The JWT token to validate + fingerprint_cookie: The fingerprint from HttpOnly cookie + + Returns: + ValidationResult with validity status and session info + """ + security_manager = get_security_manager() + return security_manager.validate_jwt_with_fingerprint( + jwt_token=jwt_token, + fingerprint_cookie=fingerprint_cookie, + ) + + +def revoke_session(session_id: str) -> None: + """ + Revoke all tokens for a session. + + Args: + session_id: The session to revoke + """ + security_manager = get_security_manager() + security_manager.revoke_session(session_id) + + # Also remove from pending sessions + if session_id in pending_websocket_sessions: + del pending_websocket_sessions[session_id] + + logger.info("session_revoked", extra={"session_id": session_id}) # Old functions related to sessions and session_metadata (get_session, remove_session, cleanup_sessions) # will be removed or heavily refactored, as business state is now managed by run_id and active_runs_store. @@ -66,4 +229,4 @@ async def create_session() -> dict: # pass # cleanup_thread = threading.Thread(target=cleanup_sessions, daemon=True) -# cleanup_thread.start() +# cleanup_thread.start() diff --git a/core/api/session_security.py b/core/api/session_security.py new file mode 100644 index 0000000..92de0cd --- /dev/null +++ b/core/api/session_security.py @@ -0,0 +1,476 @@ +""" +JWT-based session security with HttpOnly fingerprint cookie binding. + +Implements: +- RFC 8725 JWT Best Current Practices +- OWASP Token Sidejacking Prevention +- Auth0 Refresh Token Rotation Pattern + +See docs/architecture/session-resilience.md for full design documentation. +""" + +import hashlib +import secrets +import time +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from typing import Optional +import logging + +import jwt +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + + +@dataclass +class SessionSecurityConfig: + """Configuration for session security.""" + + # JWT Settings + jwt_secret: str + jwt_algorithm: str = "HS256" + jwt_expiry_minutes: int = 15 + jwt_issuer: str = "commonground" + jwt_audience: str = "commonground-ws-reconnect" + + # Refresh Token Settings + refresh_token_expiry_hours: int = 24 + refresh_token_rotation: bool = True + + # Auto-Refresh Settings + auto_refresh_threshold: float = 0.9 # 90% of lifespan + + # Fingerprint Cookie Settings + fingerprint_cookie_name: str = "__Secure-Fgp" + fingerprint_bytes: int = 32 # 256 bits of entropy + fingerprint_cookie_secure: bool = True + fingerprint_cookie_httponly: bool = True + fingerprint_cookie_samesite: str = "Strict" + fingerprint_cookie_max_age: int = 86400 # 24 hours + + # Client Heartbeat Settings + client_heartbeat_interval_seconds: int = 20 + client_heartbeat_timeout_seconds: int = 10 + client_max_missed_heartbeats: int = 2 + + +class TokenPayload(BaseModel): + """Structured JWT token payload.""" + + # Standard claims + iss: str # Issuer + sub: str # Subject (session_id) + aud: str # Audience + exp: int # Expiry timestamp + iat: int # Issued at timestamp + nbf: int # Not before timestamp + jti: str # JWT ID (unique, for revocation) + + # Custom claims + pid: str # Project ID + ver: int = 1 # Token version (for forced revocation) + fph: str # Fingerprint hash (SHA256 of cookie value) + + +class SessionTokens(BaseModel): + """Response model for session token creation.""" + + session_id: str + jwt_token: str + refresh_token: str + expires_in: int # Seconds until JWT expires + fingerprint: str # Raw fingerprint (to set in cookie) + + +class RefreshResult(BaseModel): + """Response model for token refresh.""" + + jwt_token: str + refresh_token: str + expires_in: int + + +class ValidationResult(BaseModel): + """Result of JWT validation.""" + + valid: bool + session_id: Optional[str] = None + project_id: Optional[str] = None + error: Optional[str] = None + token_payload: Optional[TokenPayload] = None + + +@dataclass +class RefreshTokenState: + """State for a refresh token.""" + + token_hash: str # SHA256 of the refresh token + session_id: str + project_id: str + fingerprint_hash: str + expires_at: datetime + used: bool = False + token_version: int = 1 + + +class SessionSecurityManager: + """ + Manages JWT session tokens with fingerprint cookie binding. + + Security properties: + - JWT signed with HS256 (configurable) + - HttpOnly fingerprint cookie prevents XSS token theft + - Refresh token rotation detects token reuse + - Explicit audience/issuer validation (RFC 8725) + """ + + def __init__(self, config: SessionSecurityConfig): + self.config = config + # In-memory store for refresh tokens (use Redis in production) + self._refresh_tokens: dict[str, RefreshTokenState] = {} + # Token version per session (for forced revocation) + self._session_versions: dict[str, int] = {} + # Revoked JTIs (for explicit token revocation) + self._revoked_jtis: set[str] = set() + + def _generate_fingerprint(self) -> str: + """Generate a cryptographically secure random fingerprint.""" + return secrets.token_urlsafe(self.config.fingerprint_bytes) + + def _hash_fingerprint(self, fingerprint: str) -> str: + """Hash fingerprint using SHA256.""" + return hashlib.sha256(fingerprint.encode()).hexdigest() + + def _hash_refresh_token(self, token: str) -> str: + """Hash refresh token for storage.""" + return hashlib.sha256(token.encode()).hexdigest() + + def _generate_jti(self) -> str: + """Generate unique JWT ID.""" + return secrets.token_urlsafe(16) + + def _generate_refresh_token(self) -> str: + """Generate secure refresh token.""" + return secrets.token_urlsafe(32) + + def _get_session_version(self, session_id: str) -> int: + """Get current token version for session.""" + return self._session_versions.get(session_id, 1) + + def create_session_tokens( + self, + session_id: str, + project_id: str, + existing_fingerprint: Optional[str] = None, + ) -> SessionTokens: + """ + Create new session tokens (JWT + refresh token + fingerprint). + + Args: + session_id: The session identifier + project_id: The project identifier + existing_fingerprint: Optional existing fingerprint for reconnection + + Returns: + SessionTokens with JWT, refresh token, and fingerprint + """ + now = datetime.now(timezone.utc) + now_ts = int(now.timestamp()) + exp_ts = int((now + timedelta(minutes=self.config.jwt_expiry_minutes)).timestamp()) + + # Use existing fingerprint for reconnection, or generate new + fingerprint = existing_fingerprint or self._generate_fingerprint() + fingerprint_hash = self._hash_fingerprint(fingerprint) + + # Get or initialize session version + version = self._get_session_version(session_id) + if session_id not in self._session_versions: + self._session_versions[session_id] = version + + # Create JWT payload + payload = TokenPayload( + iss=self.config.jwt_issuer, + sub=session_id, + aud=self.config.jwt_audience, + exp=exp_ts, + iat=now_ts, + nbf=now_ts, + jti=self._generate_jti(), + pid=project_id, + ver=version, + fph=fingerprint_hash, + ) + + # Encode JWT with explicit header type (RFC 8725 Section 3.11) + jwt_token = jwt.encode( + payload.model_dump(), + self.config.jwt_secret, + algorithm=self.config.jwt_algorithm, + headers={"typ": "session+jwt"}, + ) + + # Generate refresh token + refresh_token = self._generate_refresh_token() + refresh_expires = now + timedelta(hours=self.config.refresh_token_expiry_hours) + + # Store refresh token state + self._refresh_tokens[self._hash_refresh_token(refresh_token)] = RefreshTokenState( + token_hash=self._hash_refresh_token(refresh_token), + session_id=session_id, + project_id=project_id, + fingerprint_hash=fingerprint_hash, + expires_at=refresh_expires, + token_version=version, + ) + + logger.info( + f"Created session tokens for session={session_id}, " + f"project={project_id}, expires_in={self.config.jwt_expiry_minutes}m" + ) + + return SessionTokens( + session_id=session_id, + jwt_token=jwt_token, + refresh_token=refresh_token, + expires_in=self.config.jwt_expiry_minutes * 60, + fingerprint=fingerprint, + ) + + def validate_jwt_with_fingerprint( + self, + jwt_token: str, + fingerprint_cookie: Optional[str], + ) -> ValidationResult: + """ + Validate JWT and verify fingerprint binding. + + Args: + jwt_token: The JWT to validate + fingerprint_cookie: The fingerprint from HttpOnly cookie + + Returns: + ValidationResult with validity status and extracted claims + """ + try: + # Decode with all validations (RFC 8725) + payload_dict = jwt.decode( + jwt_token, + self.config.jwt_secret, + algorithms=[self.config.jwt_algorithm], + audience=self.config.jwt_audience, + issuer=self.config.jwt_issuer, + options={ + "require": ["exp", "iat", "nbf", "iss", "aud", "sub", "jti"], + "verify_exp": True, + "verify_iat": True, + "verify_nbf": True, + "verify_iss": True, + "verify_aud": True, + }, + ) + + payload = TokenPayload(**payload_dict) + + # Check if JTI has been revoked + if payload.jti in self._revoked_jtis: + logger.warning(f"Rejected revoked JWT: jti={payload.jti}") + return ValidationResult(valid=False, error="Token has been revoked") + + # Check token version (forced revocation) + current_version = self._get_session_version(payload.sub) + if payload.ver < current_version: + logger.warning( + f"Rejected outdated token version: " + f"token_ver={payload.ver}, current_ver={current_version}" + ) + return ValidationResult(valid=False, error="Token version outdated") + + # Verify fingerprint binding (OWASP Token Sidejacking Prevention) + if not fingerprint_cookie: + logger.warning("JWT validation failed: missing fingerprint cookie") + return ValidationResult(valid=False, error="Missing fingerprint cookie") + + actual_fingerprint_hash = self._hash_fingerprint(fingerprint_cookie) + if actual_fingerprint_hash != payload.fph: + logger.warning( + f"JWT validation failed: fingerprint mismatch for session={payload.sub}" + ) + return ValidationResult(valid=False, error="Fingerprint mismatch") + + logger.debug(f"JWT validated successfully for session={payload.sub}") + return ValidationResult( + valid=True, + session_id=payload.sub, + project_id=payload.pid, + token_payload=payload, + ) + + except jwt.ExpiredSignatureError: + logger.debug("JWT validation failed: token expired") + return ValidationResult(valid=False, error="Token expired") + except jwt.InvalidAudienceError: + logger.warning("JWT validation failed: invalid audience") + return ValidationResult(valid=False, error="Invalid audience") + except jwt.InvalidIssuerError: + logger.warning("JWT validation failed: invalid issuer") + return ValidationResult(valid=False, error="Invalid issuer") + except jwt.DecodeError as e: + logger.warning(f"JWT validation failed: decode error - {e}") + return ValidationResult(valid=False, error="Invalid token format") + except Exception as e: + logger.error(f"JWT validation failed: unexpected error - {e}") + return ValidationResult(valid=False, error="Validation failed") + + def refresh_tokens( + self, + refresh_token: str, + fingerprint_cookie: Optional[str], + ) -> Optional[RefreshResult]: + """ + Refresh session tokens using refresh token. + + Implements Auth0 refresh token rotation: + - Each refresh token can only be used once + - Reuse detection indicates potential token theft + + Args: + refresh_token: The refresh token + fingerprint_cookie: The fingerprint from HttpOnly cookie + + Returns: + RefreshResult with new JWT and refresh token, or None if invalid + """ + token_hash = self._hash_refresh_token(refresh_token) + state = self._refresh_tokens.get(token_hash) + + if not state: + logger.warning("Refresh token not found") + return None + + # Check if already used (reuse detection) + if state.used: + logger.warning( + f"Refresh token reuse detected for session={state.session_id}! " + f"Potential token theft - revoking all tokens for session" + ) + self.revoke_session(state.session_id) + return None + + # Check expiry + if datetime.now(timezone.utc) > state.expires_at: + logger.debug(f"Refresh token expired for session={state.session_id}") + del self._refresh_tokens[token_hash] + return None + + # Verify fingerprint matches + if not fingerprint_cookie: + logger.warning("Refresh failed: missing fingerprint cookie") + return None + + actual_fingerprint_hash = self._hash_fingerprint(fingerprint_cookie) + if actual_fingerprint_hash != state.fingerprint_hash: + logger.warning( + f"Refresh failed: fingerprint mismatch for session={state.session_id}" + ) + return None + + # Mark current refresh token as used + state.used = True + + # Generate new tokens (rotation) + new_tokens = self.create_session_tokens( + session_id=state.session_id, + project_id=state.project_id, + existing_fingerprint=fingerprint_cookie, # Reuse same fingerprint + ) + + logger.info(f"Tokens refreshed for session={state.session_id}") + + return RefreshResult( + jwt_token=new_tokens.jwt_token, + refresh_token=new_tokens.refresh_token, + expires_in=new_tokens.expires_in, + ) + + def revoke_session(self, session_id: str) -> None: + """ + Revoke all tokens for a session. + + Increments the token version, invalidating all existing JWTs. + Also removes all refresh tokens for the session. + """ + # Increment version to invalidate existing JWTs + current_version = self._session_versions.get(session_id, 1) + self._session_versions[session_id] = current_version + 1 + + # Remove all refresh tokens for this session + to_remove = [ + token_hash + for token_hash, state in self._refresh_tokens.items() + if state.session_id == session_id + ] + for token_hash in to_remove: + del self._refresh_tokens[token_hash] + + logger.info( + f"Revoked session={session_id}, " + f"new_version={current_version + 1}, " + f"removed_refresh_tokens={len(to_remove)}" + ) + + def revoke_token(self, jti: str) -> None: + """Revoke a specific JWT by its JTI.""" + self._revoked_jtis.add(jti) + logger.info(f"Revoked JWT: jti={jti}") + + def cleanup_expired(self) -> int: + """ + Clean up expired refresh tokens. + + Returns: + Number of tokens cleaned up + """ + now = datetime.now(timezone.utc) + expired = [ + token_hash + for token_hash, state in self._refresh_tokens.items() + if state.expires_at < now + ] + for token_hash in expired: + del self._refresh_tokens[token_hash] + + if expired: + logger.info(f"Cleaned up {len(expired)} expired refresh tokens") + + return len(expired) + + def get_cookie_settings(self) -> dict: + """Get cookie settings for fingerprint cookie.""" + return { + "key": self.config.fingerprint_cookie_name, + "httponly": self.config.fingerprint_cookie_httponly, + "secure": self.config.fingerprint_cookie_secure, + "samesite": self.config.fingerprint_cookie_samesite, + "max_age": self.config.fingerprint_cookie_max_age, + "path": "/", + } + + +# Global instance (initialized by app startup) +_security_manager: Optional[SessionSecurityManager] = None + + +def get_security_manager() -> SessionSecurityManager: + """Get the global security manager instance.""" + if _security_manager is None: + raise RuntimeError("SessionSecurityManager not initialized") + return _security_manager + + +def init_security_manager(config: SessionSecurityConfig) -> SessionSecurityManager: + """Initialize the global security manager.""" + global _security_manager + _security_manager = SessionSecurityManager(config) + logger.info("SessionSecurityManager initialized") + return _security_manager diff --git a/core/env.sample b/core/env.sample index 7b8553f..c2413c7 100644 --- a/core/env.sample +++ b/core/env.sample @@ -1 +1,37 @@ DEFAULT_BASE_URL=http://127.0.0.1:8765/v1 + +# Anthropic API Configuration +# ANTHROPIC_API_KEY=your_anthropic_api_key_here + +# Anthropic Beta Features - Extended Context Window (1M tokens) +# To enable 1M context for Claude Sonnet 4.5 (Principal agent only), uncomment: +# ANTHROPIC_EXTRA_HEADERS={"anthropic-beta": "context-1m-2025-08-07"} +# WARNING: This significantly increases API costs. Use only when needed. + +# Principal Agent Temperature (0.0-1.0, default 0.4) +# PRINCIPAL_TEMPERATURE=0.4 +# ============================================================================= +# Session Security Configuration +# ============================================================================= +# JWT_SECRET is REQUIRED for production. Must be at least 64 characters. +# Generate with: python -c "import secrets; print(secrets.token_urlsafe(64))" +# If not set, a random secret is generated (development only - sessions lost on restart!) +# JWT_SECRET=your_64_plus_character_secret_here + +# JWT token expiry in minutes (default: 15) +# JWT_EXPIRY_MINUTES=15 + +# Refresh token expiry in hours (default: 24) +# REFRESH_TOKEN_EXPIRY_HOURS=24 + +# Client heartbeat interval in seconds (default: 20) +# CLIENT_HEARTBEAT_INTERVAL_SECONDS=20 + +# Client heartbeat timeout in seconds (default: 10) +# CLIENT_HEARTBEAT_TIMEOUT_SECONDS=10 + +# Max missed client heartbeats before triggering reconnection (default: 2) +# CLIENT_MAX_MISSED_HEARTBEATS=2 + +# Set to false for local development over HTTP (default: true for HTTPS) +# COOKIE_SECURE=true \ No newline at end of file diff --git a/core/mcp.json b/core/mcp.json index 1914f64..730847e 100644 --- a/core/mcp.json +++ b/core/mcp.json @@ -1,9 +1,33 @@ { + "_documentation": { + "description": "MCP Server Configuration - Defines external tool servers available to agents", + "category_semantics": { + "google_related": "Google/Gemini ecosystem servers. Use 'all_google_related_mcp_servers' in profiles to include these when enabled.", + "user_specified": "User-added domain-specific servers. Use 'all_user_specified_mcp_servers' in profiles to include these when enabled.", + "uncategorized": "Servers without explicit category. NOT included in any category-based toolset. Must be referenced by explicit server name." + }, + "important_notes": [ + "The 'category' field is REQUIRED for servers to be included in category-based toolsets.", + "Servers without a 'category' field default to 'uncategorized' and will NOT be matched by 'all_user_specified_mcp_servers' or 'all_google_related_mcp_servers'.", + "Set 'enabled: true' to activate a server. Disabled servers are never included in any toolset.", + "The 'transport' field is required: 'http' for HTTP-based servers, 'stdio' for process-based servers." + ], + "adding_new_server": "To add a new user server: 1) Add entry below, 2) Set category to 'user_specified', 3) Set enabled to true, 4) Profiles using 'all_user_specified_mcp_servers' will automatically include it." + }, "mcpServers": { "G": { "transport": "http", "url": "http://localhost:8765/mcp", - "enabled": true + "enabled": false, + "category": "google_related", + "_comment": "Gemini CLI bridge - provides Google Search and web fetch capabilities" + }, + "Seats": { + "transport": "http", + "url": "http://localhost:4000/mcp", + "enabled": true, + "category": "user_specified", + "_comment": "Example user-specified domain knowledge server" } } } diff --git a/core/pyproject.toml b/core/pyproject.toml index bce457b..6b4e5a0 100644 --- a/core/pyproject.toml +++ b/core/pyproject.toml @@ -1,3 +1,14 @@ +# CommonGround Core - Python Package Configuration +# +# To regenerate requirements.txt from this file: +# uv export --no-hashes -o requirements.txt +# +# To regenerate with dev dependencies: +# uv export --no-hashes --extra dev -o requirements-dev.txt +# +# To upgrade all packages and regenerate: +# uv lock --upgrade && uv export --no-hashes -o requirements.txt + [project] name = "common-ground-agent-core" version = "0.1.0" @@ -7,6 +18,7 @@ license = {text = "Apache-2.0"} requires-python = ">=3.12" dependencies = [ "aiohttp>=3.8.0", + "anthropic>=0.40.0", "beautifulsoup4>=4.9.3", "openai>=1.0.0", "litellm>=1.30.0", @@ -47,3 +59,29 @@ dependencies = [ "fastmcp>=0.0.1", "light-embed @ git+https://github.com/chux0519/light-embed@b98fc0f1262e0fa4a140f9b2a437b83f79d4158e", ] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", + "pytest-asyncio>=0.23.0", + "pip-tools>=7.0.0", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +asyncio_mode = "auto" +addopts = "-v --tb=short" + +[tool.coverage.run] +source = ["agent_core"] +omit = ["*/tests/*", "*/__pycache__/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "if __name__ == .__main__.:", + "raise NotImplementedError", +] diff --git a/core/rag_configs/internal_project_docs.yaml b/core/rag_configs/internal_project_docs.yaml index b915866..884a1c2 100644 --- a/core/rag_configs/internal_project_docs.yaml +++ b/core/rag_configs/internal_project_docs.yaml @@ -32,10 +32,10 @@ meta_table: text_column: "chunk_text" # Column name for storing tags/keywords, must be of type VARCHAR[]. tags_column: "tags" - + # Defines which columns to return in search results. # This helps control the amount of information returned to the Agent. - retrieval_columns: + retrieval_columns: - "id" - "doc_id" - "url" @@ -60,18 +60,18 @@ embedding_table: # For writable data sources, a single string (single column) is recommended, as process_pending_embeddings # generates embeddings for only one model at a time. embedding_column: "embedding_vector" - + # Specifies the model used to generate embeddings. - # "jina-api": Indicates the use of Jina's online API service. - # It can also be a Hugging Face path for a local model, e.g., "jinaai/jina-embeddings-v3-base-zh". + # Options: "jina-api" for Jina's online API service, or a Hugging Face path for a local model. + # Example HuggingFace models: "jinaai/jina-embeddings-v3-base-zh", "Snowflake/snowflake-arctic-embed-m-v2.0" emb_model_id: "jina-api" - + # Matryoshka Relevant Dimensions (MRL) - the target dimension for the embedding vector. # For MRL models that require truncation or processing, the final dimension is specified here. # For non-MRL models, this is usually ignored, but it's best to match the model's output dimension. # Jina v3 outputs 1024 dimensions, but MRL to 128 or 256 is generally recommended. mrl_dims: 128 - + # Specifies the embedding task type for different types of text, which is required for some models (like Jina, BGE). # Used when indexing documents/passages. passage_task_type: "retrieval.passage" diff --git a/core/requirements-dev.txt b/core/requirements-dev.txt new file mode 100644 index 0000000..8834c49 --- /dev/null +++ b/core/requirements-dev.txt @@ -0,0 +1,585 @@ +# This file was autogenerated by uv via the following command: +# uv export --no-hashes --extra dev -o requirements-dev.txt +aiofiles==24.1.0 + # via common-ground-agent-core +aiohappyeyeballs==2.6.1 + # via aiohttp +aiohttp==3.12.13 + # via + # common-ground-agent-core + # langchain-community + # litellm +aiosignal==1.4.0 + # via aiohttp +aiosqlite==0.21.0 + # via common-ground-agent-core +annotated-types==0.7.0 + # via pydantic +anthropic==0.75.0 + # via common-ground-agent-core +anyio==4.9.0 + # via + # anthropic + # google-genai + # httpx + # mcp + # openai + # sse-starlette + # starlette +attrs==25.3.0 + # via + # aiohttp + # cattrs + # jsonschema + # lsprotocol + # referencing +audioop-lts==0.2.1 ; python_full_version >= '3.13' + # via + # speechrecognition + # standard-aifc +authlib==1.6.0 + # via fastmcp +azure-ai-documentintelligence==1.0.2 + # via markitdown +azure-core==1.35.0 + # via + # azure-ai-documentintelligence + # azure-identity +azure-identity==1.23.0 + # via markitdown +beautifulsoup4==4.13.4 + # via + # common-ground-agent-core + # markdownify + # markitdown +build==1.3.0 + # via pip-tools +cachetools==5.5.2 + # via google-auth +cattrs==25.1.1 + # via + # lsprotocol + # pygls +certifi==2025.6.15 + # via + # httpcore + # httpx + # requests +cffi==1.17.1 + # via + # cryptography + # zstandard +charset-normalizer==3.4.2 + # via + # markitdown + # pdfminer-six + # requests +cleantext==1.1.4 + # via common-ground-agent-core +click==8.2.1 + # via + # litellm + # magika + # nltk + # pip-tools + # typer + # uvicorn +cobble==0.1.4 + # via mammoth +colorama==0.4.6 + # via + # build + # click + # common-ground-agent-core + # pytest + # tqdm +coloredlogs==15.0.1 + # via onnxruntime +coolname==2.2.0 + # via common-ground-agent-core +coverage==7.13.0 + # via pytest-cov +cryptography==45.0.5 + # via + # authlib + # azure-identity + # msal + # pdfminer-six + # pyjwt +dataclasses-json==0.6.7 + # via langchain-community +defusedxml==0.7.1 + # via + # markitdown + # youtube-transcript-api +distro==1.9.0 + # via + # anthropic + # openai +dnspython==2.7.0 + # via email-validator +docstring-parser==0.17.0 + # via anthropic +duckdb==1.3.1 + # via common-ground-agent-core +email-validator==2.2.0 + # via pydantic +et-xmlfile==2.0.0 + # via openpyxl +exceptiongroup==1.3.0 + # via fastmcp +faiss-cpu==1.11.0 + # via common-ground-agent-core +fastapi==0.115.14 + # via common-ground-agent-core +fastmcp==2.10.1 + # via common-ground-agent-core +filelock==3.18.0 + # via huggingface-hub +flatbuffers==25.2.10 + # via onnxruntime +frozenlist==1.7.0 + # via + # aiohttp + # aiosignal +fsspec==2025.5.1 + # via huggingface-hub +google-auth==2.40.3 + # via google-genai +google-genai==1.24.0 + # via common-ground-agent-core +greenlet==3.2.3 ; (python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64') + # via sqlalchemy +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # anthropic + # fastmcp + # google-genai + # langsmith + # litellm + # mcp + # openai + # tavily-python +httpx-sse==0.4.1 + # via + # langchain-community + # mcp +huggingface-hub==0.25.2 + # via + # light-embed + # tokenizers +humanfriendly==10.0 + # via coloredlogs +idna==3.10 + # via + # anyio + # email-validator + # httpx + # requests + # yarl +importlib-metadata==8.7.0 + # via litellm +iniconfig==2.3.0 + # via pytest +isodate==0.7.2 + # via azure-ai-documentintelligence +jinja2==3.1.6 + # via litellm +jiter==0.10.0 + # via + # anthropic + # openai +joblib==1.5.1 + # via nltk +json-repair==0.40.0 + # via common-ground-agent-core +jsonpatch==1.33 + # via langchain-core +jsonpointer==3.0.0 + # via jsonpatch +jsonschema==4.24.0 + # via + # litellm + # mcp +jsonschema-specifications==2025.4.1 + # via jsonschema +langchain==0.3.26 + # via langchain-community +langchain-community==0.3.27 + # via common-ground-agent-core +langchain-core==0.3.68 + # via + # langchain + # langchain-community + # langchain-openai + # langchain-text-splitters +langchain-openai==0.3.27 + # via common-ground-agent-core +langchain-text-splitters==0.3.8 + # via + # common-ground-agent-core + # langchain +langsmith==0.4.4 + # via + # langchain + # langchain-community + # langchain-core +lark==1.2.2 + # via common-ground-agent-core +light-embed @ git+https://github.com/chux0519/light-embed@b98fc0f1262e0fa4a140f9b2a437b83f79d4158e + # via common-ground-agent-core +litellm==1.73.6.post1 + # via common-ground-agent-core +llvmlite==0.44.0 + # via numba +lsprotocol==2023.0.1 + # via + # common-ground-agent-core + # pygls +lxml==6.0.0 + # via + # markitdown + # python-pptx +magika==0.6.2 + # via markitdown +mammoth==1.9.1 + # via markitdown +markdown==3.8.2 + # via common-ground-agent-core +markdown-it-py==3.0.0 + # via rich +markdownify==1.1.0 + # via markitdown +markitdown==0.1.2 + # via common-ground-agent-core +markupsafe==3.0.2 + # via jinja2 +marshmallow==3.26.1 + # via dataclasses-json +mcp==1.10.1 + # via + # common-ground-agent-core + # fastmcp +mdurl==0.1.2 + # via markdown-it-py +mpmath==1.3.0 + # via sympy +msal==1.32.3 + # via + # azure-identity + # msal-extensions +msal-extensions==1.3.1 + # via azure-identity +multidict==6.6.3 + # via + # aiohttp + # yarl +mypy-extensions==1.1.0 + # via typing-inspect +nltk==3.9.1 + # via cleantext +numba==0.61.2 + # via common-ground-agent-core +numpy==2.2.6 + # via + # common-ground-agent-core + # faiss-cpu + # langchain-community + # light-embed + # magika + # numba + # onnxruntime + # pandas +olefile==0.47 + # via markitdown +onnxruntime==1.22.0 + # via + # light-embed + # magika +openai==1.93.0 + # via + # common-ground-agent-core + # langchain-openai + # litellm +openapi-pydantic==0.5.1 + # via fastmcp +openpyxl==3.1.5 + # via markitdown +orjson==3.10.18 ; platform_python_implementation != 'PyPy' + # via langsmith +packaging==24.2 + # via + # build + # faiss-cpu + # huggingface-hub + # langchain-core + # langsmith + # marshmallow + # onnxruntime + # pytest +pandas==2.3.0 + # via markitdown +pdfminer-six==20250506 + # via markitdown +pillow==11.3.0 + # via python-pptx +pip==25.3 + # via pip-tools +pip-tools==7.5.2 + # via common-ground-agent-core +pluggy==1.6.0 + # via + # pytest + # pytest-cov +pocketflow==0.0.2 + # via common-ground-agent-core +propcache==0.3.2 + # via + # aiohttp + # yarl +protobuf==6.31.1 + # via onnxruntime +pyasn1==0.6.1 + # via + # pyasn1-modules + # rsa +pyasn1-modules==0.4.2 + # via google-auth +pycparser==2.22 + # via cffi +pydantic==2.11.7 + # via + # anthropic + # common-ground-agent-core + # fastapi + # fastmcp + # google-genai + # langchain + # langchain-core + # langsmith + # litellm + # mcp + # openai + # openapi-pydantic + # pydantic-settings +pydantic-core==2.33.2 + # via pydantic +pydantic-settings==2.10.1 + # via + # langchain-community + # mcp +pydub==0.25.1 + # via markitdown +pygls==1.3.1 + # via common-ground-agent-core +pygments==2.19.2 + # via + # pytest + # rich +pyjwt==2.10.1 + # via msal +pymupdf==1.26.3 + # via common-ground-agent-core +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pyreadline3==3.5.4 ; sys_platform == 'win32' + # via humanfriendly +pytest==9.0.2 + # via + # common-ground-agent-core + # pytest-asyncio + # pytest-cov +pytest-asyncio==1.3.0 + # via common-ground-agent-core +pytest-cov==7.0.0 + # via common-ground-agent-core +python-dateutil==2.9.0.post0 + # via pandas +python-dotenv==1.1.1 + # via + # common-ground-agent-core + # fastmcp + # litellm + # magika + # pydantic-settings +python-json-logger==3.3.0 + # via common-ground-agent-core +python-multipart==0.0.20 + # via mcp +python-pptx==1.0.2 + # via markitdown +pytz==2025.2 + # via pandas +pyyaml==6.0.2 + # via + # common-ground-agent-core + # huggingface-hub + # langchain + # langchain-community + # langchain-core +querystring-parser==1.2.4 + # via common-ground-agent-core +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications +regex==2024.11.6 + # via + # nltk + # tiktoken +requests==2.32.4 + # via + # azure-core + # common-ground-agent-core + # google-genai + # huggingface-hub + # langchain + # langchain-community + # langsmith + # markitdown + # msal + # requests-toolbelt + # tavily-python + # tiktoken + # youtube-transcript-api +requests-toolbelt==1.0.0 + # via langsmith +rich==14.0.0 + # via + # fastmcp + # typer +rpds-py==0.26.0 + # via + # jsonschema + # referencing +rsa==4.9.1 + # via google-auth +setuptools==80.9.0 + # via pip-tools +shellingham==1.5.4 + # via typer +six==1.17.0 + # via + # azure-core + # markdownify + # python-dateutil + # querystring-parser +sniffio==1.3.1 + # via + # anthropic + # anyio + # openai +soupsieve==2.7 + # via beautifulsoup4 +speechrecognition==3.14.3 + # via markitdown +sqlalchemy==2.0.41 + # via + # langchain + # langchain-community +sse-starlette==2.3.6 + # via mcp +standard-aifc==3.13.0 ; python_full_version >= '3.13' + # via speechrecognition +standard-chunk==3.13.0 ; python_full_version >= '3.13' + # via standard-aifc +starlette==0.46.2 + # via + # fastapi + # mcp +sympy==1.14.0 + # via onnxruntime +tavily-python==0.7.9 + # via common-ground-agent-core +tenacity==8.5.0 + # via + # google-genai + # langchain-community + # langchain-core +tiktoken==0.9.0 + # via + # langchain-openai + # litellm + # tavily-python +tokenizers==0.21.1 + # via + # light-embed + # litellm +tqdm==4.67.1 + # via + # huggingface-hub + # nltk + # openai +typer==0.16.0 + # via fastmcp +typing-extensions==4.14.0 + # via + # aiosignal + # aiosqlite + # anthropic + # anyio + # azure-ai-documentintelligence + # azure-core + # azure-identity + # beautifulsoup4 + # cattrs + # exceptiongroup + # fastapi + # google-genai + # huggingface-hub + # langchain-core + # openai + # pydantic + # pydantic-core + # pytest-asyncio + # python-pptx + # referencing + # speechrecognition + # sqlalchemy + # typer + # typing-inspect + # typing-inspection +typing-inspect==0.9.0 + # via dataclasses-json +typing-inspection==0.4.1 + # via + # pydantic + # pydantic-settings +tzdata==2025.2 + # via pandas +urllib3==2.5.0 + # via requests +uvicorn==0.35.0 + # via + # common-ground-agent-core + # mcp +watchdog==6.0.0 + # via common-ground-agent-core +websockets==15.0.1 + # via + # common-ground-agent-core + # google-genai +wheel==0.45.1 + # via pip-tools +xlrd==2.0.2 + # via markitdown +xlsxwriter==3.2.5 + # via python-pptx +yarl==1.20.1 + # via aiohttp +youtube-transcript-api==1.0.3 + # via markitdown +yt-dlp==2025.6.30 + # via common-ground-agent-core +zipp==3.23.0 + # via importlib-metadata +zstandard==0.23.0 + # via langsmith diff --git a/core/requirements.txt b/core/requirements.txt index 235f34b688d4eae1daacacbbabe5b41e22a804f7..495c732480015182788b2cb29fde8f6ea7ec095d 100644 GIT binary patch literal 11344 zcmb_iOK;@35x(nJD44?}2-MRrx9v?j*Z{fYki{MXYpq53?4VELIh(zIu?K;(uR+igQu$_?4-cwOXEe#T%x17N5GfeTiQLi}i)axhx9# zDynmw%Cclt_Dl3B#($W?ueEG+oFtV@E=*mxiW3<({97v;Uh+z-mA>kT2}wJt<8ixR zZC2}umVYuE5J_1j=SH?wmN4+3lT$N>8#>Ip)oz}j;Z*I( zq-KF5^rU2^(hDz{8!A-YNG(%YxS@vUyx~;}qm5Xb2`THO&_=bekoYfg-RX<0zUFOF ze0|}K60(Z_c8ov6gm#|?GO&L~P}Q8LmxibE4BNo$@fQt`U=3_nd`c!jZ@2+-3gt^uP>$qMnt8$z?aFSL`#(~;$= z&hk{}H(V+<;@Rv0 z*WJ^rjxwfMS}?$_N(dZDVTaZIK!;-ZTx`pD-YTA5O9V3tH*<$PV`!ahb$jB8ZWvIw z5w%WOEuu^)-5|znJ{)6)ppjLoM`BWQ81#VfqDWrg4UkYa5%3b50ML_~d<71I!wZ*; zp)70D=ggom0Nqo?A}CFEyFGOmgW;vn8l^KdX&smbNmX~(BI4JS*AylwuR&%4ybdPX zXpl3ZRFc-Mam^3r3$z$oHwu6aZ+!Pz6mZy)c;19ozq|x87Ls59N_+_TzqIEwjv{9% zPcH2#$_u7XZH}PZhj!Y;Q@ih2c_ZI=Rls6M=HLLG?b~$9`mW2oqFf;oBmFUu4cWa- zraXB;f~4NBS6*fq2T6|qL0lnA_1Rz4(7E9dg+r1ZKw^iy;*WEQ#a~TcvHu(c?c*A% z$^r9VO9VK8c6>zI@%MlIE$Q6U}g4`6zV!X>5&WxCAg0=BH_>}Jo7e0`6kXC=b!0e95Ne1^WPYOvBkgwz zDawB*UTVY*lDmB{9LM@!n7J z;`j6SCs94It&zn~i5%G{RGxic47B0IxV4}m4nN7$P_=}ZY7#lhDw2lROc>e#Qcsb~ zDI_hcnNBZc?>irR{e%RSzL_Yg4g!6|$$-40Vy5+#A5|S8L^}zsg6+wW$q|WuVM4(J zDL-lk#AXUrl=%mS9FMaj$0u1S5Z=o|XOk*7e{3QQEB$nAF`D?xsmP-}4-bQAelhAM zj$*&#b#8Vtsno|Gs5pRbpgf#`8s#2V{Fo3S(VoVfG>iJMXXyAej2S2+YJ0zKxYfk% z$apHVzrhja|M}p%Dge0jd${1?;(A2JIgv z@&S*wokOA^(flX}ZLL_2lcF>57xfW=_E#5l4$!BiIoq&NIxP_+prT>(2hY={9c9)w zrhW_svC8N*7L|3$bRPu#laEzb)?GwpF7dKM{yZwfnZ-NBaRSid6?N{Eq2hfTf`JW8 z=#*`tMJ7^Y?gv=Z?9P)uq>1rmS9g@0y))igB>6SQjorA{_;N1LatH&UPCGQt{cC4lzmx^S;`=S}h z?T16pJG|9&g4>|);u!F7)Qt^H^#P97J_hx+oA+J)jM|t6%i$8h4A%ls(D&r&@WPzQ zdI!yz(T5;7GCNkeWyfuJHM(%Vqsv_ur(r{Q#c;Mp}dq)}d20%s|9Ez_7zrST-&E5KL z?HPD5SM)HqnKo7Ycu}Ko!F6u%5CJE^%ta%>H9~FL&UC0i(LowN^_q4j3@VuG2edy3 zOm;yNI%p}@9aEL590PcEI_rN=N}08|D|5q*Vo~@Lu@{`K?Pi#Uc-^J8Lk5`egovp} z4N}%{(WF7jFejwFW^z0%s?0h$89j9Z{cp;>?e}73YA)T}v=7_dT%K?Dnt1NiW8G#} z6A3!+xQ00^ryD>aCEzgFzzT3{G+e~lKipE99QM`RHoi>Lt3^@~Xtiin!bU?J@qp1J zP`5#C@l-J1T)rBv9(_ZOO}b(_$G0rUrsrIDWea~a2pojQVu znd;P63udOt)wg8^y->~-l7vqKru(_pfvPozCdUFUTcD6(g@It%>e&>|G zs3L1D7Om~~jbVj18#f1U*(MlIaIbLgi`Wq97P%OtFzE=K7U_rqD_`E;M2u=E6DCnZ zI1B7Vzd>(Q)cSt!RMs)-_frb){rquVeK%4#Qt#Qk{~Ky9+@Cewq_dCM?-zH=hUeY$ z8q5xg1ILY5(%WMmhbBFNsNrW0EklXu3*Z)apyCIC@On!;o30cV_E&*qxX5O`uDoNU z&q9N6x@a1f(^=&F|DD85D7V+mcK_fW%F39~#q5a}+KCo63hiQTm174ME|6~M_Ga$; z0hvwDEI}B4N5$&b6ONOqNB8%SO`R!IzrptB`?jM=I8vT@MF_+FBMPqs4&p9ezu+Ff zI$u~x+yev$Is|n+6S$tGpTcV^x7&<$kZ(`#$Sv{>IO&$(I#Q%?xt`PG4@#9hzTKMJ zhG5ta9YQJH^((<&Oic7LhB$l;5Z%0zFp2Hnudn<}ZfvQO&mgCnW6{BdW7>^)6-)5y z#CV|ZxiNvLy=*rRH4I|_>5o4JiEo4(DWb-hdf%=Endl*DVydU&dQPJJdtJ_vJT;=7;Hh*np2WU|uV47hqG(r>hZ$%AH-Np${jV}x{n8{&d_{Apf94cDH; z#lmB?MbxocA#{7CE2K=KqcLe6FK30X{G?3%j~kxpBOfR7cQ+5r@LDukAEdm@*^gz{ zLS0{*{{9U`W6g!W7}7l&yQ#Uv^eli`GlABcRI-m^m9$xkAls^ fjAa$QiP}7vojmlry~B*$BQ-k6$#inQJ}3JxKOg*Q literal 6302 zcmai&*=`$G5JcxWK>i``0d0{~Br8Aw{|Z-$vbdNOwS9b&I$hK3E6PqFP_lXZ_EKHF z%>48Bq+4~{ZmHi7-J&~mf9Yq`U3QJk2CMQ_Kt9*y<-Ujt6{3C#jFffzGf$Unm0GT!o#H{2zq| z^X89UZW(M$&xet>)By*{4ZaK_=X)Uw4_DorzTJmr@+lrrzz8Jg-HUv9 zI7v}G&ca++AEbFNEs12+&s>@h^6*s>cPBeUQ1v&L4X~aI)zskr0HTY)k$yDjp5!^X zbu6I#5fgL4-ew<5DKcT?7K(>D13M~z6Zo0;tq+!7$}UlY*GzYgCqIWx-$4o_=!@nw z_K=`1hCR1ZY8{%=vth8rh^E(YBROb1U}Abi79WM!Q8`*FlJ`Bzoi%024fU`I^zbob ztSl@eK6>P=M>Xrw-jj=4>49l*pN{74moZ#M4DROdCEeG+FztG#*gu%N=)EI*$Ph0v zSleBo$oEFHI_#a~nJMQT5xJXSaZ0>|UcMi7;ApJtbP(}ZO;fQcb5zW?@*b9uKbSKR zpN~Cbt+l!zC-k9>r1!c9^3-~EVB{S;T&BJ=W_SS7E8X7-YkDM%pUNIR0Cr_1bB9PO zhj4V;pI0QDyZ7Bk4ENG8YAj}F;*^={s=`OJ(jMJQGhGi~uuRV@W@Hx`L@sx-uxj*} z8L7wp2AM}8d3Vw_?z5e`h(G6Jb@IY4-qiOVk<|s&2^^UN*eISogg=u zSM%82FtyNk!0Vg-a+^Cy7=2FR zAvUV=nAg_JIeBAGeeU^U1C7k7$@=NLf3&?Av+o z;{2x1t~JrK>1}LS<{Z<|Pg34n@Vn+xYQ*oCJ}cx3&EAWdu{lW+lVpPX9g~VZ%ymWo zN6s6J8HWTDp8d~f?(2?fr<0{(W?s-!>9Q+%V@XdLdf0`sN|eObver9hF^tZFKWqa_ zNhZ6AoKN~hCt)+n=QZqcj;2BN=49q8NwlbcsIU`+dZH(vAeqMx!M!|OxS{d zRI_+@8w`h)t#&G{qP$m6ay>gfGE>^w6wDhicb!ztV}p3v1(_)DY8=@iZrJZBj9fl< z@$P{gXCckRp0g$2cGSd5*1o^X#Gv+`!jAdTdR%t0ng+7L(*oOn?QboQTMI<&UT1z7 zo8t73yyE@SV_!*l)UqGlZ{cgbSt#`3bUx35Yu)@VKk)Zex#OwUETPxDXRULRNRtge zg@Mtmzm@eej;gYCBtfLDe{Yy*bO%7je&9oK$g{+`J(uAfm*2;*QGsMK#~8wDqN$DB zn@9HEeejJZTv(Hyds-TMYSy!Xn>9C>wD|^$iefjeji}gIUC->P=N7sPn*kG~-Xg#h zS46&%Lc>A~^z@)^+!4IX^W=J`82JnDylH#75|8!ayCCIh)&CNdCLet me analyze what I found.\n\nThe codebase has a clean structure with three main files:\n1. main.py - Entry point\n2. utils.py - Utility functions\n3. config.py - Configuration management\n\nThis follows standard Python project conventions.", + }, + { + "role": "assistant", + "content": "## Final Analysis\n\nAfter examining the codebase, I found:\n- **Architecture**: Clean separation of concerns\n- **Entry Point**: main.py serves as the primary entry\n- **Utilities**: Shared functions in utils.py\n- **Configuration**: Centralized in config.py\n\nRecommendation: The structure is well-organized and maintainable.", + } + ] + + +@pytest.fixture +def sample_work_module(): + """Sample work module data for testing ingestors.""" + return { + "module_id": "WM_1", + "name": "Code Analysis", + "description": "Analyze the codebase structure", + "status": "pending_review", + "context_archive": [ + { + "messages": [{"role": "assistant", "content": "Analysis complete"}], + "deliverables": {"primary_summary": "Found 3 files"}, + "model": "claude-sonnet-4-20250514" + } + ], + "messages": [{"role": "user", "content": "old message"}], + "assignee_history": [{"agent": "Associate_001"}], + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T01:00:00Z" + } + + +@pytest.fixture +def mock_context(): + """Minimal mock context for testing.""" + class MockSubContext: + def __init__(self): + self.state = { + "messages": [], + "flags": {}, + "initial_parameters": {} + } + self.meta = {"agent_id": "test_agent"} + + def get(self, key, default=None): + return getattr(self, key, default) + + return MockSubContext() diff --git a/core/tests/test_app_config.py b/core/tests/test_app_config.py new file mode 100644 index 0000000..d10a04f --- /dev/null +++ b/core/tests/test_app_config.py @@ -0,0 +1,472 @@ +""" +Unit tests for agent_core.config.app_config module. + +This module tests MCP (Model Context Protocol) server configuration loading: +- Thread-safe configuration loading +- Environment variable and file-based config sources +- Category-based server organization +- Immutable proxy patterns for config access + +Key functions tested: +- _load_mcp_config_internal: Internal config parser +- get_mcp_server_categories: Thread-safe category accessor +- get_native_mcp_servers: Thread-safe server config accessor +- reload_mcp_config: Hot-reload functionality +""" + +import pytest +import os +import json +import tempfile +from types import MappingProxyType +from unittest.mock import patch, MagicMock + +# Import the module to test +from agent_core.config import app_config + + +class TestMCPConfigLoading: + """Tests for MCP configuration loading.""" + + @pytest.fixture(autouse=True) + def reset_config_state(self): + """Reset the module's internal state before each test.""" + # Save original state + original_loaded = app_config._mcp_config_loaded + original_servers = app_config._native_mcp_servers_internal.copy() + original_categories = app_config._mcp_server_categories_internal.copy() + + # Reset for test + app_config._mcp_config_loaded = False + app_config._native_mcp_servers_internal.clear() + app_config._mcp_server_categories_internal.clear() + + yield + + # Restore original state + app_config._mcp_config_loaded = original_loaded + app_config._native_mcp_servers_internal.clear() + app_config._native_mcp_servers_internal.update(original_servers) + app_config._mcp_server_categories_internal.clear() + app_config._mcp_server_categories_internal.update(original_categories) + + def test_load_from_json_file(self, tmp_path): + """Test loading MCP config from a JSON file.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "test_server": { + "enabled": True, + "category": "user_specified", + "transport": {"type": "stdio", "command": "test"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + assert "test_server" in servers + assert servers["test_server"]["enabled"] is True + assert categories["test_server"]["category"] == "user_specified" + + def test_load_from_env_var(self): + """Test loading MCP config from environment variable.""" + config_data = { + "mcpServers": { + "env_server": { + "enabled": True, + "category": "google_related", + "transport": {"type": "sse", "url": "http://example.com"} + } + } + } + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": "/nonexistent/path.json", + "NATIVE_MCP_SERVERS_CONFIG": json.dumps(config_data), + }): + servers, categories = app_config._load_mcp_config_internal() + + assert "env_server" in servers + assert categories["env_server"]["category"] == "google_related" + + def test_disabled_server_not_in_servers(self, tmp_path): + """Test that disabled servers are excluded from the servers dict.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "disabled_server": { + "enabled": False, + "category": "user_specified", + "transport": {"type": "stdio", "command": "test"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + # Server should be in categories but not in enabled servers + assert "disabled_server" not in servers + assert "disabled_server" in categories + assert categories["disabled_server"]["enabled"] is False + + def test_server_without_transport_excluded(self, tmp_path): + """Test servers without transport config are excluded.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "no_transport": { + "enabled": True, + "category": "user_specified" + # Missing 'transport' field + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + assert "no_transport" not in servers + + +class TestCategoryHandling: + """Tests for MCP server category handling.""" + + @pytest.fixture(autouse=True) + def reset_config_state(self): + """Reset config state for each test.""" + app_config._mcp_config_loaded = False + app_config._native_mcp_servers_internal.clear() + app_config._mcp_server_categories_internal.clear() + yield + app_config._mcp_config_loaded = False + + def test_default_category_is_uncategorized(self, tmp_path): + """Test servers without explicit category default to 'uncategorized'.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "no_category_server": { + "enabled": True, + "transport": {"type": "stdio", "command": "test"} + # No 'category' field + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + assert categories["no_category_server"]["category"] == "uncategorized" + assert categories["no_category_server"]["has_explicit_category"] is False + + def test_explicit_category_tracked(self, tmp_path): + """Test explicit category is tracked correctly.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "categorized_server": { + "enabled": True, + "category": "user_specified", + "transport": {"type": "stdio", "command": "test"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + assert categories["categorized_server"]["has_explicit_category"] is True + + def test_multiple_categories(self, tmp_path): + """Test loading servers with different categories.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "google_server": { + "enabled": True, + "category": "google_related", + "transport": {"type": "stdio", "command": "google"} + }, + "user_server": { + "enabled": True, + "category": "user_specified", + "transport": {"type": "stdio", "command": "user"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + + assert categories["google_server"]["category"] == "google_related" + assert categories["user_server"]["category"] == "user_specified" + + +class TestThreadSafety: + """Tests for thread-safe configuration access.""" + + @pytest.fixture(autouse=True) + def reset_config_state(self): + """Reset config state for each test.""" + app_config._mcp_config_loaded = False + app_config._native_mcp_servers_internal.clear() + app_config._mcp_server_categories_internal.clear() + yield + app_config._mcp_config_loaded = False + + def test_get_mcp_server_categories_returns_proxy(self, tmp_path): + """Test that get_mcp_server_categories returns immutable MappingProxyType.""" + config_file = tmp_path / "mcp.json" + config_data = {"mcpServers": {}} + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + categories = app_config.get_mcp_server_categories() + + assert isinstance(categories, MappingProxyType) + + def test_mapping_proxy_is_immutable(self, tmp_path): + """Test that MappingProxyType prevents mutation.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "test": { + "enabled": True, + "category": "test", + "transport": {"type": "stdio", "command": "test"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + categories = app_config.get_mcp_server_categories() + + with pytest.raises(TypeError): + categories["new_key"] = "value" + + def test_get_native_mcp_servers_returns_copy(self, tmp_path): + """Test that get_native_mcp_servers returns a deep copy.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "test": { + "enabled": True, + "category": "test", + "transport": {"type": "stdio", "command": "test", "args": ["a", "b"]} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers1 = app_config.get_native_mcp_servers() + servers2 = app_config.get_native_mcp_servers() + + # Should be equal but different objects + assert servers1 == servers2 + assert servers1 is not servers2 + + # Modifying one should not affect the other + servers1["test"]["transport"]["args"].append("c") + assert len(servers2["test"]["transport"]["args"]) == 2 + + def test_double_checked_locking(self, tmp_path): + """Test that config is loaded only once.""" + config_file = tmp_path / "mcp.json" + config_data = {"mcpServers": {}} + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + # First call loads + app_config._ensure_mcp_config_loaded() + assert app_config._mcp_config_loaded is True + + # Second call should not reload + with patch.object(app_config, '_load_mcp_config_internal') as mock_load: + app_config._ensure_mcp_config_loaded() + mock_load.assert_not_called() + + +class TestReloadConfig: + """Tests for configuration hot-reload.""" + + @pytest.fixture(autouse=True) + def reset_config_state(self): + """Reset config state for each test.""" + app_config._mcp_config_loaded = False + app_config._native_mcp_servers_internal.clear() + app_config._mcp_server_categories_internal.clear() + yield + app_config._mcp_config_loaded = False + + def test_reload_updates_config(self, tmp_path): + """Test that reload_mcp_config updates the configuration.""" + config_file = tmp_path / "mcp.json" + + # Initial config + config_data = { + "mcpServers": { + "initial_server": { + "enabled": True, + "category": "test", + "transport": {"type": "stdio", "command": "initial"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + # Load initial config + app_config._ensure_mcp_config_loaded() + servers = app_config.get_native_mcp_servers() + assert "initial_server" in servers + + # Update config file + config_data = { + "mcpServers": { + "updated_server": { + "enabled": True, + "category": "test", + "transport": {"type": "stdio", "command": "updated"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + # Reload + app_config.reload_mcp_config() + servers = app_config.get_native_mcp_servers() + + assert "updated_server" in servers + assert "initial_server" not in servers + + +class TestErrorHandling: + """Tests for error handling in config loading.""" + + @pytest.fixture(autouse=True) + def reset_config_state(self): + """Reset config state for each test.""" + app_config._mcp_config_loaded = False + app_config._native_mcp_servers_internal.clear() + app_config._mcp_server_categories_internal.clear() + yield + app_config._mcp_config_loaded = False + + def test_invalid_json_in_file(self, tmp_path): + """Test handling of invalid JSON in config file.""" + config_file = tmp_path / "mcp.json" + config_file.write_text("{ invalid json }") + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + # Should not raise, returns empty + servers, categories = app_config._load_mcp_config_internal() + assert servers == {} + assert categories == {} + + def test_invalid_json_in_env_var(self): + """Test handling of invalid JSON in environment variable.""" + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": "/nonexistent/path.json", + "NATIVE_MCP_SERVERS_CONFIG": "not valid json", + }): + # Should not raise, returns empty + servers, categories = app_config._load_mcp_config_internal() + assert servers == {} + assert categories == {} + + def test_missing_mcp_servers_key(self, tmp_path): + """Test handling config without mcpServers key.""" + config_file = tmp_path / "mcp.json" + config_file.write_text(json.dumps({"other_key": "value"})) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + assert servers == {} + assert categories == {} + + def test_non_dict_server_config(self, tmp_path): + """Test handling non-dict server configurations.""" + config_file = tmp_path / "mcp.json" + config_data = { + "mcpServers": { + "invalid_server": "not a dict", + "valid_server": { + "enabled": True, + "category": "test", + "transport": {"type": "stdio", "command": "test"} + } + } + } + config_file.write_text(json.dumps(config_data)) + + with patch.dict(os.environ, { + "NATIVE_MCP_SERVERS_CONFIG_PATH": str(config_file), + "NATIVE_MCP_SERVERS_CONFIG": "", + }): + servers, categories = app_config._load_mcp_config_internal() + # Invalid should be skipped, valid should be loaded + assert "invalid_server" not in servers + assert "valid_server" in servers + + +class TestModuleLevelExports: + """Tests for module-level exported constants.""" + + def test_mcp_server_categories_is_mapping_proxy(self): + """Test MCP_SERVER_CATEGORIES is exported as MappingProxyType.""" + # This tests the module-level export at import time + assert isinstance(app_config.MCP_SERVER_CATEGORIES, MappingProxyType) + + def test_native_mcp_servers_is_dict(self): + """Test NATIVE_MCP_SERVERS is exported as dict.""" + assert isinstance(app_config.NATIVE_MCP_SERVERS, dict) diff --git a/core/tests/test_base_tool_node.py b/core/tests/test_base_tool_node.py new file mode 100644 index 0000000..c6c51a6 --- /dev/null +++ b/core/tests/test_base_tool_node.py @@ -0,0 +1,546 @@ +""" +Tier 3 Unit Tests for agent_core/nodes/base_tool_node.py + +Tests the BaseToolNode class and helper functions for tool execution: +- BaseToolNode: prep_async(), post_async() +- add_tool_result_to_inbox(): Helper for adding tool results +- dehydrate_payload_recursively(): Intelligent payload dehydration + +Test Categories: +1. BaseToolNode Lifecycle: __init__, prep_async, post_async +2. Tool Result Inbox: add_tool_result_to_inbox() +3. Payload Dehydration: dehydrate_payload_recursively() +""" + +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +import json +import uuid +from datetime import datetime, timezone + +from agent_core.nodes.base_tool_node import ( + BaseToolNode, + add_tool_result_to_inbox, + dehydrate_payload_recursively +) + + +class MockToolNode(BaseToolNode): + """A mock implementation of BaseToolNode for testing.""" + + def __init__(self, **kwargs): + # Set _tool_info before calling super().__init__ + self._tool_info = { + "name": "mock_tool", + "description": "A mock tool for testing", + "toolset": "test" + } + super().__init__(**kwargs) + + async def exec_async(self, prep_res): + """Mock implementation.""" + return { + "status": "success", + "payload": {"result": "test result"} + } + + +class TestBaseToolNodeInit: + """Tests for BaseToolNode initialization.""" + + def test_init_with_tool_info(self): + """Test that decorated nodes initialize properly.""" + node = MockToolNode() + assert node._tool_info["name"] == "mock_tool" + + def test_init_without_tool_info_raises(self): + """Test that undecorated nodes raise TypeError.""" + class UnDecoratedNode(BaseToolNode): + async def exec_async(self, prep_res): + return {} + + with pytest.raises(TypeError, match="was not decorated with @tool_registry"): + UnDecoratedNode() + + +class TestBaseToolNodePrepAsync: + """Tests for BaseToolNode.prep_async().""" + + @pytest.mark.asyncio + async def test_prep_extracts_tool_params(self): + """Test that prep_async extracts parameters from current_action.""" + node = MockToolNode() + shared = { + "state": { + "current_action": { + "type": "tool_call", + "tool_name": "mock_tool", + "tool_call_id": "tc_123", + "implementation_type": "custom", + "query": "search query", + "limit": 10 + } + } + } + + result = await node.prep_async(shared) + + assert result["tool_params"]["query"] == "search query" + assert result["tool_params"]["limit"] == 10 + # Reserved keywords should be excluded + assert "type" not in result["tool_params"] + assert "tool_name" not in result["tool_params"] + assert "tool_call_id" not in result["tool_params"] + assert "implementation_type" not in result["tool_params"] + + @pytest.mark.asyncio + async def test_prep_returns_shared_context(self): + """Test that prep_async includes shared_context in result.""" + node = MockToolNode() + shared = {"state": {"current_action": {}}, "refs": {}} + + result = await node.prep_async(shared) + + assert result["shared_context"] is shared + + @pytest.mark.asyncio + async def test_prep_handles_empty_state(self): + """Test prep_async with minimal shared dict.""" + node = MockToolNode() + shared = {} + + result = await node.prep_async(shared) + + assert result["tool_params"] == {} + assert result["shared_context"] is shared + + +class TestBaseToolNodePostAsync: + """Tests for BaseToolNode.post_async().""" + + @pytest.mark.asyncio + async def test_post_adds_success_result_to_inbox(self): + """Test that successful results are added to inbox.""" + node = MockToolNode() + shared = { + "state": { + "current_action": {}, + "current_tool_call_id": "tc_456", + "inbox": [] + }, + "refs": {"run": {"runtime": {}}} + } + prep_res = {"tool_params": {}, "shared_context": shared} + exec_res = { + "status": "success", + "payload": {"answer": "42"} + } + + result = await node.post_async(shared, prep_res, exec_res) + + assert result == "default" + assert len(shared["state"]["inbox"]) == 1 + inbox_item = shared["state"]["inbox"][0] + assert inbox_item["source"] == "TOOL_RESULT" + assert inbox_item["payload"]["tool_name"] == "mock_tool" + assert inbox_item["payload"]["is_error"] is False + + @pytest.mark.asyncio + async def test_post_adds_error_result_to_inbox(self): + """Test that error results are added to inbox with error content.""" + node = MockToolNode() + shared = { + "state": { + "current_action": {}, + "current_tool_call_id": "tc_789", + "inbox": [] + }, + "refs": {"run": {"runtime": {}}} + } + prep_res = {"tool_params": {}, "shared_context": shared} + exec_res = { + "status": "error", + "payload": None, + "error_message": "Something went wrong" + } + + await node.post_async(shared, prep_res, exec_res) + + inbox_item = shared["state"]["inbox"][0] + assert inbox_item["payload"]["is_error"] is True + assert inbox_item["payload"]["content"]["error"] == "Something went wrong" + + @pytest.mark.asyncio + async def test_post_clears_current_action(self): + """Test that post_async clears current_action.""" + node = MockToolNode() + shared = { + "state": { + "current_action": {"tool_name": "mock_tool"}, + "inbox": [] + }, + "refs": {"run": {"runtime": {}}} + } + prep_res = {"tool_params": {}, "shared_context": shared} + exec_res = {"status": "success", "payload": {}} + + await node.post_async(shared, prep_res, exec_res) + + assert shared["state"]["current_action"] is None + + @pytest.mark.asyncio + async def test_post_handles_knowledge_base_items(self): + """Test that _knowledge_items_to_add are processed.""" + node = MockToolNode() + mock_kb = AsyncMock() + + shared = { + "state": { + "current_action": {}, + "current_tool_call_id": "tc_kb", + "inbox": [] + }, + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "meta": {"agent_id": "test_agent"} + } + prep_res = {"tool_params": {}, "shared_context": shared} + exec_res = { + "status": "success", + "payload": {}, + "_knowledge_items_to_add": [ + {"content": "fact 1", "metadata": {}}, + {"content": "fact 2", "metadata": {"extra": "data"}} + ] + } + + await node.post_async(shared, prep_res, exec_res) + + assert mock_kb.add_item.call_count == 2 + # Check metadata was added + first_call = mock_kb.add_item.call_args_list[0][0][0] + assert first_call["metadata"]["source_tool_name"] == "mock_tool" + + @pytest.mark.asyncio + async def test_post_skips_kb_on_error(self): + """Test that knowledge base items are not added on error.""" + node = MockToolNode() + mock_kb = AsyncMock() + + shared = { + "state": {"current_action": {}, "inbox": []}, + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}} + } + prep_res = {"tool_params": {}, "shared_context": shared} + exec_res = { + "status": "error", + "payload": None, + "_knowledge_items_to_add": [{"content": "should not add"}] + } + + await node.post_async(shared, prep_res, exec_res) + + mock_kb.add_item.assert_not_called() + + +class TestAddToolResultToInbox: + """Tests for the add_tool_result_to_inbox helper.""" + + @pytest.mark.asyncio + async def test_adds_result_with_all_fields(self): + """Test adding a complete tool result.""" + state = {"inbox": []} + + await add_tool_result_to_inbox( + state=state, + tool_name="test_tool", + tool_call_id="tc_001", + is_error=False, + content={"result": "success"} + ) + + assert len(state["inbox"]) == 1 + item = state["inbox"][0] + assert item["source"] == "TOOL_RESULT" + assert item["payload"]["tool_name"] == "test_tool" + assert item["payload"]["tool_call_id"] == "tc_001" + assert item["payload"]["is_error"] is False + assert item["payload"]["content"] == {"result": "success"} + assert item["consumption_policy"] == "consume_on_read" + assert "created_at" in item["metadata"] + + @pytest.mark.asyncio + async def test_creates_inbox_if_missing(self): + """Test that inbox is created if not present.""" + state = {} + + await add_tool_result_to_inbox( + state=state, + tool_name="tool", + tool_call_id=None, + is_error=False, + content="test" + ) + + assert "inbox" in state + assert len(state["inbox"]) == 1 + + @pytest.mark.asyncio + async def test_appends_to_existing_inbox(self): + """Test that results are appended to existing inbox.""" + state = {"inbox": [{"existing": "item"}]} + + await add_tool_result_to_inbox( + state=state, + tool_name="tool", + tool_call_id="tc", + is_error=False, + content="new" + ) + + assert len(state["inbox"]) == 2 + assert state["inbox"][0]["existing"] == "item" + + @pytest.mark.asyncio + async def test_handles_error_result(self): + """Test adding an error result.""" + state = {"inbox": []} + + await add_tool_result_to_inbox( + state=state, + tool_name="failing_tool", + tool_call_id="tc_fail", + is_error=True, + content={"error": "Operation failed"} + ) + + item = state["inbox"][0] + assert item["payload"]["is_error"] is True + + @pytest.mark.asyncio + async def test_item_id_is_unique(self): + """Test that each item gets a unique ID.""" + state = {"inbox": []} + + await add_tool_result_to_inbox(state, "t1", "tc1", False, "c1") + await add_tool_result_to_inbox(state, "t2", "tc2", False, "c2") + + ids = [item["item_id"] for item in state["inbox"]] + assert ids[0] != ids[1] + assert all(id.startswith("inbox_") for id in ids) + + +class TestDehydratePayloadRecursively: + """Tests for the dehydrate_payload_recursively function.""" + + @pytest.mark.asyncio + async def test_no_kb_returns_data_unchanged(self): + """Test that without KB, data is returned unchanged.""" + context = {"refs": {"run": {"runtime": {}}}} + tool_info = {"name": "test_tool"} + data = {"key": "value", "nested": {"inner": "data"}} + + result = await dehydrate_payload_recursively(data, context, tool_info) + + assert result == data + + @pytest.mark.asyncio + async def test_small_data_not_dehydrated(self): + """Test that small data is not dehydrated.""" + mock_kb = AsyncMock() + mock_kb.store_with_token = AsyncMock(return_value="<#CGKB-token>") + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + data = {"small": "data"} # Well under 1KB + + result = await dehydrate_payload_recursively(data, context, tool_info) + + assert result == data + mock_kb.store_with_token.assert_not_called() + + @pytest.mark.asyncio + async def test_large_string_dehydrated(self): + """Test that large strings are dehydrated.""" + mock_kb = AsyncMock() + mock_kb.store_with_token = AsyncMock(return_value="<#CGKB-bigstring>") + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {"current_tool_call_id": "tc_str"} + } + tool_info = {"name": "test_tool"} + large_string = "x" * 2000 # Over 1KB + + result = await dehydrate_payload_recursively(large_string, context, tool_info) + + assert result == "<#CGKB-bigstring>" + mock_kb.store_with_token.assert_called_once() + + @pytest.mark.asyncio + async def test_large_dict_value_dehydrated(self): + """Test that large dict values are dehydrated.""" + mock_kb = AsyncMock() + mock_kb.store_with_token = AsyncMock(return_value="<#CGKB-largevalue>") + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + data = { + "small_key": "small_value", + "large_key": "y" * 2000 + } + + result = await dehydrate_payload_recursively(data, context, tool_info) + + assert result["small_key"] == "small_value" + assert result["large_key"] == "<#CGKB-largevalue>" + + @pytest.mark.asyncio + async def test_large_list_item_dehydrated(self): + """Test that large list items are dehydrated.""" + mock_kb = AsyncMock() + mock_kb.store_with_token = AsyncMock(return_value="<#CGKB-listitem>") + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + data = ["small", "z" * 2000, "also small"] + + result = await dehydrate_payload_recursively(data, context, tool_info) + + assert result[0] == "small" + assert result[1] == "<#CGKB-listitem>" + assert result[2] == "also small" + + @pytest.mark.asyncio + async def test_nested_dehydration(self): + """Test that nested structures are processed recursively.""" + mock_kb = AsyncMock() + mock_kb.store_with_token = AsyncMock(return_value="<#CGKB-nested>") + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + # The dehydration checks individual key-value pairs; make inner content large enough + data = { + "level1": { + "level2": { + "large_content": "a" * 2000 + } + } + } + + result = await dehydrate_payload_recursively(data, context, tool_info) + + # The dehydration may occur at different levels depending on size calculation + # Check that store_with_token was called at least once + assert mock_kb.store_with_token.called + + @pytest.mark.asyncio + async def test_metadata_includes_tool_info(self): + """Test that dehydration metadata includes tool info.""" + mock_kb = AsyncMock() + stored_metadata = None + + async def capture_metadata(content, metadata): + nonlocal stored_metadata + stored_metadata = metadata + return "<#CGKB-captured>" + + mock_kb.store_with_token = capture_metadata + + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {"current_tool_call_id": "tc_meta"} + } + tool_info = {"name": "capture_tool"} + data = "b" * 2000 + + await dehydrate_payload_recursively(data, context, tool_info) + + assert stored_metadata["item_type"] == "DEHYDRATED_TOOL_PAYLOAD_PART" + assert stored_metadata["source_tool_name"] == "capture_tool" + assert stored_metadata["tool_call_id"] == "tc_meta" + assert stored_metadata["original_path"] == "payload" + + @pytest.mark.asyncio + async def test_primitives_unchanged(self): + """Test that primitive types are returned unchanged.""" + mock_kb = AsyncMock() + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + + assert await dehydrate_payload_recursively(42, context, tool_info) == 42 + assert await dehydrate_payload_recursively(3.14, context, tool_info) == 3.14 + assert await dehydrate_payload_recursively(True, context, tool_info) is True + assert await dehydrate_payload_recursively(None, context, tool_info) is None + assert await dehydrate_payload_recursively("short", context, tool_info) == "short" + + @pytest.mark.asyncio + async def test_empty_structures(self): + """Test handling of empty structures.""" + mock_kb = AsyncMock() + context = { + "refs": {"run": {"runtime": {"knowledge_base": mock_kb}}}, + "state": {} + } + tool_info = {"name": "test_tool"} + + assert await dehydrate_payload_recursively({}, context, tool_info) == {} + assert await dehydrate_payload_recursively([], context, tool_info) == [] + + +class TestBaseToolNodeExecAsync: + """Tests for the abstract exec_async method.""" + + @pytest.mark.asyncio + async def test_exec_must_be_overridden(self): + """Test that exec_async raises NotImplementedError in base class.""" + class PartialNode(BaseToolNode): + def __init__(self): + self._tool_info = {"name": "partial"} + super().__init__() + + # Deliberately NOT implementing exec_async + + # Create a class that inherits but doesn't implement + class NoExecNode(PartialNode): + pass + + node = NoExecNode() + with pytest.raises(NotImplementedError): + await node.exec_async({}) + + @pytest.mark.asyncio + async def test_exec_implementation_called(self): + """Test that implemented exec_async is called correctly.""" + exec_called = False + + class ImplementedNode(BaseToolNode): + def __init__(self): + self._tool_info = {"name": "implemented"} + super().__init__() + + async def exec_async(self, prep_res): + nonlocal exec_called + exec_called = True + return {"status": "success", "payload": prep_res.get("tool_params")} + + node = ImplementedNode() + result = await node.exec_async({"tool_params": {"key": "value"}}) + + assert exec_called + assert result["status"] == "success" + assert result["payload"] == {"key": "value"} diff --git a/core/tests/test_call_llm.py b/core/tests/test_call_llm.py new file mode 100644 index 0000000..92f65bd --- /dev/null +++ b/core/tests/test_call_llm.py @@ -0,0 +1,814 @@ +""" +Unit tests for agent_core/llm/call_llm.py + +Tests token estimation, response aggregation, and LLM call orchestration. +""" + +import pytest +import asyncio +from unittest.mock import patch, MagicMock, AsyncMock +import json +import os + +from agent_core.llm.call_llm import ( + estimate_prompt_tokens, + LLMResponseAggregator, + FunctionCallErrorException, + call_litellm_acompletion, +) + + +class TestEstimatePromptTokens: + """Tests for the estimate_prompt_tokens function. + + Note: estimate_prompt_tokens now delegates to token_counter.count_tokens. + These tests verify the integration works correctly. + """ + + @patch("agent_core.llm.token_counter.count_tokens") + def test_estimates_tokens_for_text(self, mock_counter): + """Test token estimation for plain text.""" + mock_counter.return_value = 10 + + result = estimate_prompt_tokens(model="gpt-4", text="Hello world") + + mock_counter.assert_called_once() + call_args = mock_counter.call_args + assert call_args[1]["model"] == "gpt-4" + assert call_args[1]["text"] == "Hello world" + assert result == 10 + + @patch("agent_core.llm.token_counter.count_tokens") + def test_estimates_tokens_for_messages(self, mock_counter): + """Test token estimation for message list.""" + mock_counter.return_value = 25 + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"} + ] + + result = estimate_prompt_tokens(model="gpt-4", messages=messages) + + assert result == 25 + call_args = mock_counter.call_args + assert call_args[1]["messages"] == messages + + @patch("agent_core.llm.token_counter.count_tokens") + def test_passes_system_prompt(self, mock_counter): + """Test that system_prompt is passed through.""" + mock_counter.return_value = 30 + + result = estimate_prompt_tokens( + model="gpt-4", + text="Hello", + system_prompt="You are helpful" + ) + + call_args = mock_counter.call_args + assert call_args[1]["system_prompt"] == "You are helpful" + + @patch("agent_core.llm.token_counter.count_tokens") + def test_raises_for_both_text_and_messages(self, mock_counter): + """Test that providing both text and messages raises ValueError.""" + mock_counter.side_effect = ValueError("Provide either 'text' or 'messages', not both.") + + with pytest.raises(ValueError, match="not both"): + estimate_prompt_tokens( + model="gpt-4", + text="Hello", + messages=[{"role": "user", "content": "World"}] + ) + + @patch("agent_core.llm.token_counter.count_tokens") + def test_returns_zero_for_no_model(self, mock_counter): + """Test that missing model returns 0.""" + mock_counter.return_value = 0 + + result = estimate_prompt_tokens(model="", text="Hello") + + assert result == 0 + + @patch("agent_core.llm.token_counter.count_tokens") + def test_returns_zero_for_empty_input(self, mock_counter): + """Test that empty input returns 0.""" + mock_counter.return_value = 0 + + result = estimate_prompt_tokens(model="gpt-4") + + assert result == 0 + + @patch("agent_core.llm.token_counter.count_tokens") + def test_passes_llm_config(self, mock_counter): + """Test that llm_config is passed through.""" + mock_counter.return_value = 15 + config = {"litellm_token_counter_model": "gpt-3.5-turbo"} + + result = estimate_prompt_tokens( + model="custom-model", + text="Hello", + llm_config_for_tokenizer=config + ) + + call_args = mock_counter.call_args + assert call_args[1]["llm_config"] == config + + @patch("agent_core.llm.token_counter.count_tokens") + def test_handles_token_counter_exception(self, mock_counter): + """Test graceful handling of token_counter exceptions.""" + # The underlying count_tokens handles exceptions and returns 0 + mock_counter.return_value = 0 + + result = estimate_prompt_tokens(model="gpt-4", text="Hello") + + assert result == 0 + + +class TestLLMResponseAggregator: + """Tests for the LLMResponseAggregator class.""" + + @pytest.fixture + def aggregator(self): + """Create a basic aggregator for testing.""" + return LLMResponseAggregator( + agent_id="test-agent", + parent_agent_id=None, + events=None, + run_id="test-run", + stream_id="test-stream", + llm_model_id="gpt-4" + ) + + def test_initialization(self, aggregator): + """Test aggregator initializes with correct defaults.""" + assert aggregator.agent_id == "test-agent" + assert aggregator.full_content == "" + assert aggregator.full_reasoning_content == "" + assert aggregator.current_tool_call_chunks == {} + assert aggregator.raw_chunks == [] + assert aggregator.model_id_used is None + assert aggregator.actual_usage is None + + @pytest.mark.asyncio + async def test_process_chunk_content(self, aggregator): + """Test processing a content chunk.""" + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Hello" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + + await aggregator.process_chunk(chunk) + + assert aggregator.full_content == "Hello" + assert aggregator.model_id_used == "gpt-4" + + @pytest.mark.asyncio + async def test_process_chunk_accumulates_content(self, aggregator): + """Test that multiple content chunks are accumulated.""" + for text in ["Hello", " ", "World"]: + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = text + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + + await aggregator.process_chunk(chunk) + + assert aggregator.full_content == "Hello World" + + @pytest.mark.asyncio + async def test_process_chunk_reasoning_content(self, aggregator): + """Test processing reasoning content.""" + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = None + chunk.choices[0].delta.reasoning_content = "Let me think..." + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + + await aggregator.process_chunk(chunk) + + assert aggregator.full_reasoning_content == "Let me think..." + + @pytest.mark.asyncio + async def test_process_chunk_detects_tool_call_tag(self, aggregator): + """Test that tag triggers retry exception.""" + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "some_tool" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + + with pytest.raises(FunctionCallErrorException, match="tool_call"): + await aggregator.process_chunk(chunk) + + @pytest.mark.asyncio + async def test_process_chunk_detects_tool_code_tag(self, aggregator): + """Test that tag triggers retry exception.""" + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "print('hello')" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + + with pytest.raises(FunctionCallErrorException, match="tool_code"): + await aggregator.process_chunk(chunk) + + @pytest.mark.asyncio + async def test_process_chunk_tool_calls(self, aggregator): + """Test processing tool call chunks.""" + # First chunk with tool call start + chunk1 = MagicMock() + chunk1.choices = [MagicMock()] + chunk1.choices[0].delta = MagicMock() + chunk1.choices[0].delta.content = None + chunk1.choices[0].delta.reasoning_content = None + tc_chunk = MagicMock() + tc_chunk.index = 0 + tc_chunk.id = "call_123" + tc_chunk.function = MagicMock() + tc_chunk.function.name = "search" + tc_chunk.function.arguments = '{"query":' + chunk1.choices[0].delta.tool_calls = [tc_chunk] + chunk1.model = "gpt-4" + chunk1.usage = None + + await aggregator.process_chunk(chunk1) + + assert 0 in aggregator.current_tool_call_chunks + assert aggregator.current_tool_call_chunks[0]["id"] == "call_123" + assert aggregator.current_tool_call_chunks[0]["function"]["name"] == "search" + + @pytest.mark.asyncio + async def test_process_chunk_captures_usage(self, aggregator): + """Test that usage information is captured from chunks.""" + chunk = MagicMock() + chunk.choices = [] + chunk.usage = MagicMock() + chunk.usage.dict.return_value = {"prompt_tokens": 100, "completion_tokens": 50} + + await aggregator.process_chunk(chunk) + + assert aggregator.actual_usage == {"prompt_tokens": 100, "completion_tokens": 50} + + @pytest.mark.asyncio + async def test_process_chunk_no_choices(self, aggregator): + """Test handling chunk with no choices.""" + chunk = MagicMock() + chunk.choices = [] + chunk.usage = None + + # Should not raise + await aggregator.process_chunk(chunk) + + assert aggregator.full_content == "" + + def test_get_aggregated_response(self, aggregator): + """Test getting aggregated response.""" + aggregator.full_content = "Hello world" + aggregator.full_reasoning_content = "Let me think" + aggregator.model_id_used = "gpt-4" + aggregator.actual_usage = {"prompt_tokens": 10, "completion_tokens": 5} + + result = aggregator.get_aggregated_response(messages_for_llm=[]) + + assert result["content"] == "Hello world" + assert result["reasoning"] == "Let me think" + assert result["model_id_used"] == "gpt-4" + assert result["actual_usage"]["prompt_tokens"] == 10 + + def test_get_aggregated_response_repairs_json(self, aggregator): + """Test that tool call arguments JSON is repaired.""" + aggregator.current_tool_call_chunks = { + 0: { + "id": "call_1", + "type": "function", + "function": { + "name": "test_tool", + "arguments": '{"key": "value"' # Missing closing brace + } + } + } + + result = aggregator.get_aggregated_response(messages_for_llm=[]) + + # json_repair should fix the JSON + tool_calls = result["tool_calls"] + assert len(tool_calls) == 1 + # The arguments should be parseable now + args = json.loads(tool_calls[0]["function"]["arguments"]) + assert args["key"] == "value" + + +class TestFunctionCallErrorException: + """Tests for the FunctionCallErrorException.""" + + def test_exception_message(self): + """Test exception stores message correctly.""" + exc = FunctionCallErrorException("Test error message") + + assert str(exc) == "Test error message" + + def test_exception_is_exception_subclass(self): + """Test that it's a proper Exception subclass.""" + exc = FunctionCallErrorException("Test") + + assert isinstance(exc, Exception) + + +class TestCallLitellmAcompletion: + """Tests for the call_litellm_acompletion function.""" + + @pytest.fixture + def basic_config(self): + """Basic LLM config for testing.""" + return { + "model": "gpt-4", + "max_retries": 2, + "wait_seconds_on_retry": 0.1 + } + + @pytest.fixture + def mock_events(self): + """Create mock events object.""" + events = AsyncMock() + events.emit_llm_stream_started = AsyncMock() + events.emit_llm_request_params = AsyncMock() + events.emit_llm_chunk = AsyncMock() + events.emit_llm_stream_ended = AsyncMock() + events.emit_llm_stream_failed = AsyncMock() + events.send_json = AsyncMock() + return events + + @pytest.mark.asyncio + async def test_returns_error_for_missing_model(self, basic_config): + """Test that missing model returns error dict.""" + config = {"max_retries": 1} # No model + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hello"}], + llm_config=config + ) + + # The function catches ValueError and returns error dict + assert "error" in result + assert "model" in result["error"].lower() or "Unexpected" in result["error"] + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_successful_call(self, mock_acompletion, basic_config): + """Test successful LLM call returns aggregated response.""" + # Create mock streaming response + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Hello!" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + mock_acompletion.return_value = mock_stream() + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert result["content"] == "Hello!" + assert "final_stream_id" in result + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_prepends_system_prompt(self, mock_acompletion, basic_config): + """Test that system_prompt_content is prepended to messages.""" + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Response" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + mock_acompletion.return_value = mock_stream() + + await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config, + system_prompt_content="You are helpful" + ) + + call_args = mock_acompletion.call_args + messages = call_args[1]["messages"] + assert messages[0]["role"] == "system" + assert messages[0]["content"] == "You are helpful" + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_includes_tools_in_request(self, mock_acompletion, basic_config): + """Test that tools are included in the request.""" + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Using tool" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + mock_acompletion.return_value = mock_stream() + tools = [{"type": "function", "function": {"name": "test", "parameters": {}}}] + + await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config, + api_tools_list=tools, + tool_choice="auto" + ) + + call_args = mock_acompletion.call_args + assert call_args[1]["tools"] == tools + assert call_args[1]["tool_choice"] == "auto" + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_retries_on_empty_response(self, mock_acompletion, basic_config): + """Test that empty response triggers retry.""" + call_count = 0 + + async def mock_stream_factory(): + nonlocal call_count + call_count += 1 + if call_count == 1: + # First call returns empty + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "" # Empty + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + else: + # Second call returns content + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Valid response" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + mock_acompletion.side_effect = lambda **kwargs: mock_stream_factory() + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert call_count == 2 + assert result["content"] == "Valid response" + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_returns_error_on_authentication_error(self, mock_acompletion, basic_config): + """Test that AuthenticationError returns error dict without retry.""" + from litellm.exceptions import AuthenticationError + + mock_acompletion.side_effect = AuthenticationError( + message="Invalid API key", + llm_provider="openai", + model="gpt-4" + ) + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert "error" in result + assert result["error_type"] == "AuthenticationError" + # Should only be called once (no retry for auth errors) + assert mock_acompletion.call_count == 1 + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_returns_error_on_context_window_exceeded(self, mock_acompletion, basic_config): + """Test that ContextWindowExceededError returns error without retry.""" + from litellm.exceptions import ContextWindowExceededError + + mock_acompletion.side_effect = ContextWindowExceededError( + message="Context too long", + llm_provider="openai", + model="gpt-4" + ) + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert "error" in result + assert result["error_type"] == "ContextWindowExceededError" + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_retries_on_rate_limit_error(self, mock_acompletion, basic_config): + """Test that RateLimitError triggers retry.""" + from litellm.exceptions import RateLimitError + + call_count = 0 + + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Success" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + def side_effect(**kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + raise RateLimitError( + message="Rate limited", + llm_provider="openai", + model="gpt-4" + ) + return mock_stream() + + mock_acompletion.side_effect = side_effect + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert call_count == 2 + assert result["content"] == "Success" + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_emits_events_when_provided(self, mock_acompletion, basic_config, mock_events): + """Test that events are emitted when events object is provided.""" + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Hello" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + chunk.usage = None + yield chunk + + mock_acompletion.return_value = mock_stream() + + await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config, + events=mock_events, + agent_id_for_event="test-agent", + run_id_for_event="test-run" + ) + + mock_events.emit_llm_stream_started.assert_called_once() + mock_events.emit_llm_request_params.assert_called_once() + mock_events.emit_llm_stream_ended.assert_called_once() + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_updates_token_usage_stats(self, mock_acompletion, basic_config): + """Test that token usage stats are updated in run_context.""" + async def mock_stream(): + chunk = MagicMock() + chunk.choices = [MagicMock()] + chunk.choices[0].delta = MagicMock() + chunk.choices[0].delta.content = "Hello" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.model = "gpt-4" + # Usage chunk + chunk.usage = MagicMock() + chunk.usage.dict.return_value = {"prompt_tokens": 100, "completion_tokens": 50} + yield chunk + + mock_acompletion.return_value = mock_stream() + + run_context = { + "runtime": { + "token_usage_stats": { + "total_prompt_tokens": 0, + "total_completion_tokens": 0, + "total_successful_calls": 0, + "total_failed_calls": 0, + "max_context_window": 0 + } + } + } + + await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config, + run_context=run_context + ) + + stats = run_context["runtime"]["token_usage_stats"] + assert stats["total_prompt_tokens"] == 100 + assert stats["total_completion_tokens"] == 50 + assert stats["total_successful_calls"] == 1 + + @pytest.mark.asyncio + async def test_handles_cancellation(self, basic_config): + """Test that asyncio.CancelledError is propagated.""" + with patch("agent_core.llm.call_llm.litellm.acompletion") as mock_acompletion: + mock_acompletion.side_effect = asyncio.CancelledError() + + with pytest.raises(asyncio.CancelledError): + await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + @pytest.mark.asyncio + @patch("agent_core.llm.call_llm.litellm.acompletion") + async def test_exhausted_retries_returns_error(self, mock_acompletion, basic_config): + """Test that exhausted retries returns error dict.""" + from litellm.exceptions import RateLimitError + + mock_acompletion.side_effect = RateLimitError( + message="Rate limited", + llm_provider="openai", + model="gpt-4" + ) + + result = await call_litellm_acompletion( + messages=[{"role": "user", "content": "Hi"}], + llm_config=basic_config + ) + + assert "error" in result + assert "failed after all retries" in result["error"] + # max_retries=2 means 3 total attempts (0, 1, 2) + assert mock_acompletion.call_count == 3 + + +class TestLLMResponseAggregatorContextualData: + """Tests for contextual data handling in LLMResponseAggregator.""" + + def test_get_contextual_data_empty(self): + """Test contextual data returns None when empty.""" + aggregator = LLMResponseAggregator( + agent_id="test", + parent_agent_id=None, + events=None, + run_id="run", + stream_id="stream", + llm_model_id="gpt-4" + ) + + result = aggregator._get_contextual_data_for_event() + + assert result is None + + def test_get_contextual_data_with_task_nums(self): + """Test contextual data includes task nums.""" + aggregator = LLMResponseAggregator( + agent_id="test", + parent_agent_id=None, + events=None, + run_id="run", + stream_id="stream", + llm_model_id="gpt-4", + associated_task_nums_for_event=[1, 2, 3] + ) + + result = aggregator._get_contextual_data_for_event() + + assert result["associated_task_nums"] == [1, 2, 3] + + def test_get_contextual_data_with_module_id(self): + """Test contextual data includes module_id.""" + aggregator = LLMResponseAggregator( + agent_id="test", + parent_agent_id=None, + events=None, + run_id="run", + stream_id="stream", + llm_model_id="gpt-4", + module_id_for_event="module_123" + ) + + result = aggregator._get_contextual_data_for_event() + + assert result["module_id"] == "module_123" + + def test_get_contextual_data_with_dispatch_id(self): + """Test contextual data includes dispatch_id.""" + aggregator = LLMResponseAggregator( + agent_id="test", + parent_agent_id=None, + events=None, + run_id="run", + stream_id="stream", + llm_model_id="gpt-4", + dispatch_id_for_event="dispatch_456" + ) + + result = aggregator._get_contextual_data_for_event() + + assert result["dispatch_id"] == "dispatch_456" + + +class TestFilteredKeysForLiteLLM: + """Tests for parameter filtering before sending to LiteLLM API. + + These tests verify that internal config keys are properly filtered + out before making API calls to prevent 'Extra inputs not permitted' errors. + """ + + def test_filtered_keys_constant_includes_max_context_tokens(self): + """Test that FILTERED_KEYS includes max_context_tokens. + + This tests the fix for Anthropic API error: + 'max_context_tokens: Extra inputs are not permitted' + + max_context_tokens is used by Context Budget Guardian internally + and should NOT be passed to the LiteLLM/Anthropic API. + """ + # Import the function to inspect its code + import inspect + from agent_core.llm.call_llm import call_litellm_acompletion + + source = inspect.getsource(call_litellm_acompletion) + + # Verify max_context_tokens is in the FILTERED_KEYS list + assert 'max_context_tokens' in source + assert 'FILTERED_KEYS' in source + + def test_internal_keys_not_passed_to_api(self): + """Test that internal config keys are properly filtered.""" + # These keys should be filtered before sending to LiteLLM + internal_keys = [ + "stream_id", + "parent_agent_id", + "wait_seconds_on_retry", + "max_retries", + "max_context_tokens" # Used by Context Budget Guardian only + ] + + # Simulate base_params with internal keys + base_params = { + "model": "anthropic/claude-sonnet-4-5-20250929", + "messages": [{"role": "user", "content": "Hello"}], + "temperature": 0.4, + "stream": True, + # Internal keys that should be filtered + "stream_id": "test-stream-123", + "parent_agent_id": "Partner", + "wait_seconds_on_retry": 5, + "max_retries": 2, + "max_context_tokens": 1000000 # Context Budget Guardian config + } + + # Apply the same filtering logic as call_litellm_acompletion + FILTERED_KEYS = ["stream_id", "parent_agent_id", "wait_seconds_on_retry", "max_retries", "max_context_tokens"] + params_for_litellm = {k: v for k, v in base_params.items() if k not in FILTERED_KEYS} + + # Verify internal keys are filtered out + for key in internal_keys: + assert key not in params_for_litellm, f"{key} should be filtered from API params" + + # Verify API-relevant keys remain + assert "model" in params_for_litellm + assert "messages" in params_for_litellm + assert "temperature" in params_for_litellm + assert "stream" in params_for_litellm diff --git a/core/tests/test_clean_messages_for_llm.py b/core/tests/test_clean_messages_for_llm.py new file mode 100644 index 0000000..371c453 --- /dev/null +++ b/core/tests/test_clean_messages_for_llm.py @@ -0,0 +1,548 @@ +""" +Unit tests for BaseAgentNode._clean_messages_for_llm method. + +This module tests the message cleaning/sanitization that occurs before +sending messages to the LLM API. Critical functionality includes: +- Removing internal fields (those starting with _) +- Ensuring content is always a string +- Sanitizing tool_calls to ensure arguments are valid JSON dictionaries + (required by Anthropic's API: tool_use.input must be a dictionary) + +The tool_call sanitization was added to fix a bug where malformed tool +arguments from the LLM (e.g., "" instead of {}) would cause the next +API call to fail with "tool_use.input: Input should be a valid dictionary". +""" + +import pytest +import json +from unittest.mock import MagicMock, patch + +from agent_core.nodes.base_agent_node import AgentNode + + +class MockAgentNode(AgentNode): + """A mock implementation of AgentNode for testing _clean_messages_for_llm.""" + + def __init__(self): + # Skip the full __init__ - we only need the method under test + pass + + +@pytest.fixture +def agent_node(): + """Create a mock agent node for testing.""" + return MockAgentNode() + + +class TestCleanMessagesBasic: + """Tests for basic message cleaning functionality.""" + + def test_preserves_standard_fields(self, agent_node): + """Test that standard LLM fields are preserved.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ] + result = agent_node._clean_messages_for_llm(messages) + + assert len(result) == 2 + assert result[0] == {"role": "user", "content": "Hello"} + assert result[1] == {"role": "assistant", "content": "Hi there!"} + + def test_removes_internal_fields(self, agent_node): + """Test that internal fields (starting with _) are removed.""" + messages = [ + { + "role": "user", + "content": "Hello", + "_internal_id": "abc123", + "_no_handover": True, + "_timestamp": "2025-01-01", + } + ] + result = agent_node._clean_messages_for_llm(messages) + + assert len(result) == 1 + assert "_internal_id" not in result[0] + assert "_no_handover" not in result[0] + assert "_timestamp" not in result[0] + assert result[0] == {"role": "user", "content": "Hello"} + + def test_converts_none_content_to_empty_string(self, agent_node): + """Test that None content is converted to empty string.""" + messages = [{"role": "assistant", "content": None}] + result = agent_node._clean_messages_for_llm(messages) + + assert result[0]["content"] == "" + + def test_converts_dict_content_to_json_string(self, agent_node): + """Test that dict content is converted to JSON string.""" + messages = [{"role": "user", "content": {"key": "value", "num": 42}}] + result = agent_node._clean_messages_for_llm(messages) + + assert isinstance(result[0]["content"], str) + parsed = json.loads(result[0]["content"]) + assert parsed == {"key": "value", "num": 42} + + def test_preserves_tool_call_id_and_name(self, agent_node): + """Test that tool_call_id and name fields are preserved.""" + messages = [ + {"role": "tool", "tool_call_id": "call-123", "name": "search", "content": "results"} + ] + result = agent_node._clean_messages_for_llm(messages) + + assert result[0]["tool_call_id"] == "call-123" + assert result[0]["name"] == "search" + assert result[0]["content"] == "results" + + +class TestToolCallsSanitization: + """Tests for tool_calls sanitization - the critical fix for Anthropic API compatibility.""" + + def test_valid_tool_calls_pass_through(self, agent_node): + """Test that valid tool_calls with proper JSON arguments pass through.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "search", + "arguments": '{"query": "python docs"}' + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + assert len(result[0]["tool_calls"]) == 1 + tc = result[0]["tool_calls"][0] + assert tc["id"] == "call-1" + assert tc["function"]["name"] == "search" + # Arguments should be valid JSON that parses to a dict + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {"query": "python docs"} + + def test_empty_string_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that empty string arguments are sanitized to empty dict. + + This is the primary bug fix case - LLM returns "" instead of "{}" + which causes Anthropic to reject the next API call. + """ + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "dispatch_submodules", + "arguments": '""' # Malformed - should be "{}" + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} # Should be sanitized to empty dict + + def test_non_dict_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that non-dict JSON arguments are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "some_tool", + "arguments": '"just a string"' # Valid JSON, but not a dict + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_invalid_json_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that invalid JSON arguments are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "broken_tool", + "arguments": '{invalid json}' # Not valid JSON + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_null_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that null JSON arguments are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "null_tool", + "arguments": 'null' # Valid JSON null + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_array_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that array JSON arguments are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "array_tool", + "arguments": '["item1", "item2"]' # Valid JSON array, not dict + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_number_arguments_sanitized_to_empty_dict(self, agent_node): + """Test that number JSON arguments are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "number_tool", + "arguments": '42' # Valid JSON number, not dict + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_multiple_tool_calls_all_sanitized(self, agent_node): + """Test that all tool_calls in a message are sanitized.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "valid_tool", + "arguments": '{"key": "value"}' # Valid + } + }, + { + "id": "call-2", + "function": { + "name": "invalid_tool", + "arguments": '""' # Invalid - empty string + } + }, + { + "id": "call-3", + "function": { + "name": "broken_tool", + "arguments": 'not json at all' # Invalid JSON + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tool_calls = result[0]["tool_calls"] + assert len(tool_calls) == 3 + + # First should be preserved + assert json.loads(tool_calls[0]["function"]["arguments"]) == {"key": "value"} + + # Second and third should be sanitized + assert json.loads(tool_calls[1]["function"]["arguments"]) == {} + assert json.loads(tool_calls[2]["function"]["arguments"]) == {} + + def test_tool_call_other_fields_preserved(self, agent_node): + """Test that other tool_call fields are preserved during sanitization.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-unique-123", + "type": "function", + "function": { + "name": "my_special_tool", + "arguments": '""' # Will be sanitized + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + assert tc["id"] == "call-unique-123" + assert tc["type"] == "function" + assert tc["function"]["name"] == "my_special_tool" + + def test_missing_arguments_defaults_to_empty_dict(self, agent_node): + """Test that missing arguments field defaults to empty dict.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "no_args_tool" + # No "arguments" key at all + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + def test_empty_dict_string_arguments_preserved(self, agent_node): + """Test that '{}' arguments are preserved correctly.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "empty_args_tool", + "arguments": '{}' # Valid empty dict + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed == {} + + +class TestToolCallsLogging: + """Tests for logging behavior during tool_calls sanitization.""" + + def test_logs_warning_for_non_dict_arguments(self, agent_node): + """Test that a warning is logged when arguments are not a dict.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "test_tool", + "arguments": '"string_value"' + } + } + ] + }] + + with patch('agent_core.nodes.base_agent_node.logger') as mock_logger: + agent_node._clean_messages_for_llm(messages) + + # Should have logged a warning about sanitization + mock_logger.warning.assert_called() + call_args = mock_logger.warning.call_args + assert call_args[0][0] == "tool_call_arguments_sanitized" + assert call_args[1]["extra"]["tool_name"] == "test_tool" + assert call_args[1]["extra"]["reason"] == "not_a_dict" + + def test_logs_warning_for_json_decode_error(self, agent_node): + """Test that a warning is logged for JSON decode errors.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "broken_tool", + "arguments": 'not valid json' + } + } + ] + }] + + with patch('agent_core.nodes.base_agent_node.logger') as mock_logger: + agent_node._clean_messages_for_llm(messages) + + mock_logger.warning.assert_called() + call_args = mock_logger.warning.call_args + assert call_args[0][0] == "tool_call_arguments_sanitized" + assert call_args[1]["extra"]["tool_name"] == "broken_tool" + assert call_args[1]["extra"]["reason"] == "json_decode_error" + + def test_no_warning_for_valid_arguments(self, agent_node): + """Test that no warning is logged for valid dict arguments.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "valid_tool", + "arguments": '{"valid": "args"}' + } + } + ] + }] + + with patch('agent_core.nodes.base_agent_node.logger') as mock_logger: + agent_node._clean_messages_for_llm(messages) + + # Should NOT have called warning + mock_logger.warning.assert_not_called() + + +class TestEdgeCases: + """Tests for edge cases and unusual inputs.""" + + def test_empty_messages_list(self, agent_node): + """Test handling of empty messages list.""" + result = agent_node._clean_messages_for_llm([]) + assert result == [] + + def test_message_without_tool_calls(self, agent_node): + """Test that messages without tool_calls are handled correctly.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi!"}, + ] + result = agent_node._clean_messages_for_llm(messages) + + assert "tool_calls" not in result[0] + assert "tool_calls" not in result[1] + + def test_tool_call_without_function_key(self, agent_node): + """Test handling of malformed tool_call without function key.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1" + # No "function" key + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + # Should not crash, tool_call should pass through + assert len(result[0]["tool_calls"]) == 1 + assert result[0]["tool_calls"][0]["id"] == "call-1" + + def test_does_not_mutate_original_messages(self, agent_node): + """Test that original messages are not mutated.""" + original_args = '""' + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "test_tool", + "arguments": original_args + } + } + ] + }] + + # Store original reference + original_func = messages[0]["tool_calls"][0]["function"] + + result = agent_node._clean_messages_for_llm(messages) + + # Original should be unchanged + assert messages[0]["tool_calls"][0]["function"]["arguments"] == original_args + # Result should be sanitized + assert json.loads(result[0]["tool_calls"][0]["function"]["arguments"]) == {} + + def test_unicode_in_arguments(self, agent_node): + """Test that unicode characters in arguments are preserved.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "unicode_tool", + "arguments": '{"text": "γ“γ‚“γ«γ‘γ―δΈ–η•Œ", "emoji": "πŸŽ‰"}' + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed["text"] == "γ“γ‚“γ«γ‘γ―δΈ–η•Œ" + assert parsed["emoji"] == "πŸŽ‰" + + def test_nested_dict_in_arguments(self, agent_node): + """Test that nested dicts in arguments are preserved.""" + messages = [{ + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "nested_tool", + "arguments": '{"outer": {"inner": {"deep": "value"}}}' + } + } + ] + }] + result = agent_node._clean_messages_for_llm(messages) + + tc = result[0]["tool_calls"][0] + parsed = json.loads(tc["function"]["arguments"]) + assert parsed["outer"]["inner"]["deep"] == "value" diff --git a/core/tests/test_config_resolver.py b/core/tests/test_config_resolver.py new file mode 100644 index 0000000..4f98da0 --- /dev/null +++ b/core/tests/test_config_resolver.py @@ -0,0 +1,403 @@ +""" +Unit tests for agent_core.llm.config_resolver module. + +This module tests the LLMConfigResolver class that converts YAML-based +LLM configurations into final LiteLLM parameters, with support for +environment variables and file-based secrets. + +Key functionality tested: +- _recursive_resolve: Directive parsing (_type: from_env, json_from_file) +- Environment variable resolution with defaults +- JSON parsing from env vars +- Boolean/null conversion from string env vars +""" + +import pytest +import os +import json +import tempfile +from unittest.mock import patch, MagicMock +from agent_core.llm.config_resolver import LLMConfigResolver + + +class TestRecursiveResolveFromEnv: + """Tests for _recursive_resolve with from_env directive.""" + + @pytest.fixture + def resolver(self): + """Create a resolver with empty shared configs.""" + return LLMConfigResolver({}) + + def test_simple_env_var_resolution(self, resolver): + """Test resolving a simple environment variable.""" + config = {"_type": "from_env", "var": "TEST_VAR"} + + with patch.dict(os.environ, {"TEST_VAR": "test_value"}): + result = resolver._recursive_resolve(config) + + assert result == "test_value" + + def test_env_var_with_default_when_set(self, resolver): + """Test env var with default when var is set.""" + config = {"_type": "from_env", "var": "SET_VAR", "default": "fallback"} + + with patch.dict(os.environ, {"SET_VAR": "actual_value"}): + result = resolver._recursive_resolve(config) + + assert result == "actual_value" + + def test_env_var_with_default_when_unset(self, resolver): + """Test env var with default when var is not set.""" + config = {"_type": "from_env", "var": "UNSET_VAR_XYZ", "default": "fallback"} + + # Ensure the var is not set + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("UNSET_VAR_XYZ", None) + result = resolver._recursive_resolve(config) + + assert result == "fallback" + + def test_required_env_var_missing_raises(self, resolver): + """Test that missing required env var raises ValueError.""" + config = {"_type": "from_env", "var": "REQUIRED_MISSING", "required": True} + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("REQUIRED_MISSING", None) + with pytest.raises(ValueError) as exc_info: + resolver._recursive_resolve(config) + + assert "REQUIRED_MISSING" in str(exc_info.value) + + def test_env_var_converts_true_string_to_bool(self, resolver): + """Test that 'true' string is converted to boolean True.""" + config = {"_type": "from_env", "var": "BOOL_VAR"} + + with patch.dict(os.environ, {"BOOL_VAR": "true"}): + result = resolver._recursive_resolve(config) + + assert result is True + + def test_env_var_converts_false_string_to_bool(self, resolver): + """Test that 'false' string is converted to boolean False.""" + config = {"_type": "from_env", "var": "BOOL_VAR"} + + with patch.dict(os.environ, {"BOOL_VAR": "FALSE"}): # Case insensitive + result = resolver._recursive_resolve(config) + + assert result is False + + def test_env_var_converts_null_string_to_none(self, resolver): + """Test that 'null' string is converted to None.""" + config = {"_type": "from_env", "var": "NULL_VAR"} + + with patch.dict(os.environ, {"NULL_VAR": "null"}): + result = resolver._recursive_resolve(config) + + assert result is None + + def test_env_var_parses_json_object(self, resolver): + """Test that JSON object string is parsed.""" + config = {"_type": "from_env", "var": "JSON_VAR"} + json_value = '{"key": "value", "count": 42}' + + with patch.dict(os.environ, {"JSON_VAR": json_value}): + result = resolver._recursive_resolve(config) + + assert result == {"key": "value", "count": 42} + + def test_env_var_parses_json_array(self, resolver): + """Test that JSON array string is parsed.""" + config = {"_type": "from_env", "var": "ARRAY_VAR"} + json_value = '["a", "b", "c"]' + + with patch.dict(os.environ, {"ARRAY_VAR": json_value}): + result = resolver._recursive_resolve(config) + + assert result == ["a", "b", "c"] + + def test_env_var_invalid_json_returns_string(self, resolver): + """Test that invalid JSON is returned as string.""" + config = {"_type": "from_env", "var": "BAD_JSON"} + + with patch.dict(os.environ, {"BAD_JSON": "{not valid json"}): + result = resolver._recursive_resolve(config) + + # Should return as string since JSON parsing failed + assert result == "{not valid json" + + def test_missing_var_key_raises(self, resolver): + """Test that missing 'var' key raises ValueError.""" + config = {"_type": "from_env"} # Missing 'var' + + with pytest.raises(ValueError) as exc_info: + resolver._recursive_resolve(config) + + assert "var" in str(exc_info.value) + + def test_unset_optional_env_var_returns_none(self, resolver): + """Test that unset optional env var returns None.""" + config = {"_type": "from_env", "var": "OPTIONAL_UNSET"} + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("OPTIONAL_UNSET", None) + result = resolver._recursive_resolve(config) + + assert result is None + + +class TestRecursiveResolveJsonFromFile: + """Tests for _recursive_resolve with json_from_file directive.""" + + @pytest.fixture + def resolver(self): + return LLMConfigResolver({}) + + def test_loads_json_from_file(self, resolver): + """Test loading JSON content from file.""" + with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: + json.dump({"loaded": "from_file", "number": 123}, f) + temp_path = f.name + + try: + config = {"_type": "json_from_file", "path": temp_path} + result = resolver._recursive_resolve(config) + + assert result == {"loaded": "from_file", "number": 123} + finally: + os.unlink(temp_path) + + def test_missing_path_key_raises(self, resolver): + """Test that missing 'path' key raises ValueError.""" + config = {"_type": "json_from_file"} # Missing 'path' + + with pytest.raises(ValueError) as exc_info: + resolver._recursive_resolve(config) + + assert "path" in str(exc_info.value) + + def test_nonexistent_file_raises(self, resolver): + """Test that nonexistent file raises FileNotFoundError.""" + config = {"_type": "json_from_file", "path": "/nonexistent/path/file.json"} + + with pytest.raises(FileNotFoundError): + resolver._recursive_resolve(config) + + +class TestRecursiveResolvePassthrough: + """Tests for _recursive_resolve with non-directive values.""" + + @pytest.fixture + def resolver(self): + return LLMConfigResolver({}) + + def test_passthrough_string(self, resolver): + """Test that plain string passes through unchanged.""" + result = resolver._recursive_resolve("plain_string") + assert result == "plain_string" + + def test_passthrough_number(self, resolver): + """Test that numbers pass through unchanged.""" + assert resolver._recursive_resolve(42) == 42 + assert resolver._recursive_resolve(3.14) == 3.14 + + def test_passthrough_none(self, resolver): + """Test that None passes through unchanged.""" + assert resolver._recursive_resolve(None) is None + + def test_passthrough_list(self, resolver): + """Test that list without _type passes through.""" + data = [1, 2, 3] + assert resolver._recursive_resolve(data) == [1, 2, 3] + + def test_passthrough_dict_without_type(self, resolver): + """Test that dict without _type passes through.""" + data = {"key": "value", "nested": {"inner": True}} + assert resolver._recursive_resolve(data) == data + + def test_unknown_type_directive_returns_as_is(self, resolver): + """Test that unknown _type directive returns config as-is.""" + config = {"_type": "unknown_directive", "data": "value"} + result = resolver._recursive_resolve(config) + + assert result == config + + +class TestResolverResolveMethod: + """Tests for the main resolve method.""" + + def test_resolve_requires_llm_config_ref(self): + """Test that missing llm_config_ref raises ValueError.""" + resolver = LLMConfigResolver({}) + profile = {"name": "TestProfile"} # Missing llm_config_ref + + with pytest.raises(ValueError) as exc_info: + resolver.resolve(profile) + + assert "llm_config_ref" in str(exc_info.value) + + def test_resolve_raises_for_missing_config(self): + """Test that missing referenced config raises ValueError.""" + resolver = LLMConfigResolver({}) + profile = {"name": "TestProfile", "llm_config_ref": "NonExistent"} + + # Mock at source since it's a delayed import + with patch("agent_profiles.loader.get_active_llm_config_by_name") as mock: + mock.return_value = None + + with pytest.raises(ValueError) as exc_info: + resolver.resolve(profile) + + assert "NonExistent" in str(exc_info.value) + + def test_resolve_processes_config_values(self): + """Test that resolve processes all config values.""" + shared_configs = {} + resolver = LLMConfigResolver(shared_configs) + profile = {"name": "TestProfile", "llm_config_ref": "TestConfig"} + + base_config = { + "name": "TestConfig", + "is_active": True, + "config": { + "model": "gpt-4", + "temperature": 0.7, + "api_key": {"_type": "from_env", "var": "API_KEY"}, + } + } + + with patch("agent_profiles.loader.get_active_llm_config_by_name") as mock: + mock.return_value = base_config + + with patch.dict(os.environ, {"API_KEY": "secret-key"}): + result = resolver.resolve(profile) + + assert result["model"] == "gpt-4" + assert result["temperature"] == 0.7 + assert result["api_key"] == "secret-key" + + def test_resolve_filters_none_values(self): + """Test that None values are filtered from final params.""" + resolver = LLMConfigResolver({}) + profile = {"name": "TestProfile", "llm_config_ref": "TestConfig"} + + base_config = { + "name": "TestConfig", + "is_active": True, + "config": { + "model": "gpt-4", + "optional_param": {"_type": "from_env", "var": "UNSET_OPTIONAL"}, + } + } + + with patch("agent_profiles.loader.get_active_llm_config_by_name") as mock: + mock.return_value = base_config + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("UNSET_OPTIONAL", None) + result = resolver.resolve(profile) + + assert "model" in result + assert "optional_param" not in result # None value filtered out + + +class TestEnvVarEdgeCases: + """Edge case tests for environment variable handling.""" + + @pytest.fixture + def resolver(self): + return LLMConfigResolver({}) + + def test_whitespace_in_json_env_var(self, resolver): + """Test JSON parsing with leading/trailing whitespace.""" + config = {"_type": "from_env", "var": "WHITESPACE_JSON"} + + with patch.dict(os.environ, {"WHITESPACE_JSON": ' {"key": "value"} '}): + result = resolver._recursive_resolve(config) + + assert result == {"key": "value"} + + def test_empty_string_env_var(self, resolver): + """Test handling of empty string env var.""" + config = {"_type": "from_env", "var": "EMPTY_VAR"} + + with patch.dict(os.environ, {"EMPTY_VAR": ""}): + result = resolver._recursive_resolve(config) + + assert result == "" + + def test_integer_string_converted_to_int(self, resolver): + """Test that integer strings are converted to int for API compatibility.""" + config = {"_type": "from_env", "var": "NUMBER_STR"} + + with patch.dict(os.environ, {"NUMBER_STR": "12345"}): + result = resolver._recursive_resolve(config) + + # Should be converted to int for API compatibility (e.g., max_tokens) + assert result == 12345 + assert isinstance(result, int) + + def test_float_string_converted_to_float(self, resolver): + """Test that float strings are converted to float for API compatibility.""" + config = {"_type": "from_env", "var": "FLOAT_STR"} + + with patch.dict(os.environ, {"FLOAT_STR": "0.4"}): + result = resolver._recursive_resolve(config) + + # Should be converted to float for API compatibility (e.g., temperature) + assert result == 0.4 + assert isinstance(result, float) + + def test_temperature_string_converted_to_float(self, resolver): + """Test that temperature env var string is properly converted to float. + + This tests the fix for Anthropic API error: + 'temperature: Input should be a valid number' + """ + config = {"_type": "from_env", "var": "PRINCIPAL_TEMPERATURE", "default": 0.4} + + # Simulate env var set as string (common when exported in shell) + with patch.dict(os.environ, {"PRINCIPAL_TEMPERATURE": "0.7"}): + result = resolver._recursive_resolve(config) + + assert result == 0.7 + assert isinstance(result, float) + + def test_scientific_notation_converted_to_float(self, resolver): + """Test that scientific notation strings are converted to float.""" + config = {"_type": "from_env", "var": "SCI_NUM"} + + with patch.dict(os.environ, {"SCI_NUM": "1e-5"}): + result = resolver._recursive_resolve(config) + + assert result == 1e-5 + assert isinstance(result, float) + + def test_non_numeric_string_stays_string(self, resolver): + """Test that non-numeric strings remain as strings.""" + config = {"_type": "from_env", "var": "TEXT_STR"} + + with patch.dict(os.environ, {"TEXT_STR": "hello-world"}): + result = resolver._recursive_resolve(config) + + assert result == "hello-world" + assert isinstance(result, str) + + def test_mixed_case_bool_conversion(self, resolver): + """Test boolean conversion is case-insensitive.""" + config = {"_type": "from_env", "var": "MIXED_BOOL"} + + for bool_str in ["True", "TRUE", "true", "TrUe"]: + with patch.dict(os.environ, {"MIXED_BOOL": bool_str}): + result = resolver._recursive_resolve(config) + assert result is True + + def test_default_can_be_any_type(self, resolver): + """Test that default value can be any type.""" + # Default as dict + config = {"_type": "from_env", "var": "UNSET", "default": {"nested": "default"}} + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("UNSET", None) + result = resolver._recursive_resolve(config) + + assert result == {"nested": "default"} diff --git a/core/tests/test_connection_manager.py b/core/tests/test_connection_manager.py new file mode 100644 index 0000000..48884fe --- /dev/null +++ b/core/tests/test_connection_manager.py @@ -0,0 +1,597 @@ +""" +Unit tests for the Connection Manager module. + +Tests cover: +- Connection lifecycle (register, unregister) +- Heartbeat state management +- Grace period handling +- Event buffering +- Run state management +- Reconnection logic +""" + +import pytest +import asyncio +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, MagicMock, patch +import sys +import os + +# Add the core directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from api.connection_manager import ( + ConnectionManager, + ConnectionState, + ConnectionConfig, + RunConnectionState, + HeartbeatState, + BufferedEvent +) + + +# ============================================================================= +# Fixtures +# ============================================================================= + +@pytest.fixture +def fresh_connection_manager(): + """Create a fresh ConnectionManager instance for each test.""" + # Reset singleton for testing + ConnectionManager._instance = None + manager = ConnectionManager() + yield manager + # Clean up + ConnectionManager._instance = None + + +@pytest.fixture +def mock_websocket(): + """Create a mock WebSocket.""" + ws = MagicMock() + ws.send_json = AsyncMock() + ws.send_text = AsyncMock() + return ws + + +@pytest.fixture +def mock_event_manager(): + """Create a mock SessionEventManager.""" + em = MagicMock() + em.session_id = "test-session-123" + em.emit_system_event = AsyncMock() + em.emit_raw = AsyncMock() + return em + + +# ============================================================================= +# HeartbeatState Tests +# ============================================================================= + +class TestHeartbeatState: + """Tests for HeartbeatState dataclass.""" + + def test_initial_state(self): + """HeartbeatState starts with no pings/pongs and zero missed.""" + state = HeartbeatState() + assert state.last_ping_sent is None + assert state.last_pong_received is None + assert state.missed_heartbeats == 0 + assert state.heartbeat_task is None + + def test_record_ping(self): + """Recording a ping updates last_ping_sent.""" + state = HeartbeatState() + before = datetime.now() + state.record_ping() + after = datetime.now() + + assert state.last_ping_sent is not None + assert before <= state.last_ping_sent <= after + + def test_record_pong_resets_missed(self): + """Recording a pong resets missed heartbeats to zero.""" + state = HeartbeatState() + state.missed_heartbeats = 5 + + state.record_pong() + + assert state.missed_heartbeats == 0 + assert state.last_pong_received is not None + + def test_record_missed_increments(self): + """Recording missed increments the counter.""" + state = HeartbeatState() + + assert state.record_missed() == 1 + assert state.record_missed() == 2 + assert state.record_missed() == 3 + assert state.missed_heartbeats == 3 + + +# ============================================================================= +# BufferedEvent Tests +# ============================================================================= + +class TestBufferedEvent: + """Tests for BufferedEvent dataclass.""" + + def test_event_creation(self): + """BufferedEvent stores event data correctly.""" + event = BufferedEvent( + timestamp=datetime.now(), + event_type="test_event", + event_data={"key": "value"}, + run_id="test-run" + ) + + assert event.event_type == "test_event" + assert event.event_data == {"key": "value"} + assert event.run_id == "test-run" + + def test_event_not_expired(self): + """Recent events are not expired.""" + event = BufferedEvent( + timestamp=datetime.now(), + event_type="test", + event_data={} + ) + + assert not event.is_expired(ttl_seconds=60) + + def test_event_expired(self): + """Old events are expired.""" + old_time = datetime.now() - timedelta(seconds=120) + event = BufferedEvent( + timestamp=old_time, + event_type="test", + event_data={} + ) + + assert event.is_expired(ttl_seconds=60) + + +# ============================================================================= +# RunConnectionState Tests +# ============================================================================= + +class TestRunConnectionState: + """Tests for RunConnectionState dataclass.""" + + def test_initial_state(self): + """RunConnectionState starts in CONNECTED state.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + + assert state.state == ConnectionState.CONNECTED + assert state.run_id == "test-run" + assert state.session_id == "test-session" + assert state.disconnected_at is None + assert state.grace_period_expires is None + + def test_start_grace_period(self): + """Starting grace period updates state correctly.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + + before = datetime.now() + state.start_grace_period() + after = datetime.now() + + assert state.state == ConnectionState.GRACE_PERIOD + assert state.disconnected_at is not None + assert before <= state.disconnected_at <= after + assert state.grace_period_expires is not None + + # Grace period should be in the future + expected_expiry = state.disconnected_at + timedelta( + seconds=ConnectionConfig.RECONNECTION_GRACE_PERIOD_SECONDS + ) + assert abs((state.grace_period_expires - expected_expiry).total_seconds()) < 1 + + def test_grace_period_not_expired(self): + """Grace period is not expired when within window.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + state.start_grace_period() + + assert not state.is_grace_period_expired() + + def test_grace_period_expired(self): + """Grace period is expired when past window.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + state.state = ConnectionState.GRACE_PERIOD + state.disconnected_at = datetime.now() - timedelta(seconds=200) + state.grace_period_expires = datetime.now() - timedelta(seconds=80) + + assert state.is_grace_period_expired() + + def test_reconnect(self): + """Reconnecting resets state to CONNECTED.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + state.start_grace_period() + + state.reconnect() + + assert state.state == ConnectionState.CONNECTED + assert state.disconnected_at is None + assert state.grace_period_expires is None + + def test_buffer_event(self): + """Events can be buffered.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + + state.buffer_event("test_type", {"data": "value"}) + + assert len(state.event_buffer) == 1 + event = state.event_buffer[0] + assert event.event_type == "test_type" + assert event.event_data == {"data": "value"} + + def test_get_buffered_events_clears(self): + """Getting buffered events clears the buffer by default.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + state.buffer_event("event1", {}) + state.buffer_event("event2", {}) + + events = state.get_buffered_events(clear=True) + + assert len(events) == 2 + assert len(state.event_buffer) == 0 + + def test_get_buffered_events_preserves(self): + """Getting buffered events can preserve the buffer.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + state.buffer_event("event1", {}) + + events = state.get_buffered_events(clear=False) + + assert len(events) == 1 + assert len(state.event_buffer) == 1 + + def test_save_checkpoint(self): + """Checkpoints can be saved.""" + state = RunConnectionState(run_id="test-run", session_id="test-session") + checkpoint_data = {"status": "running", "turn_count": 5} + + state.save_checkpoint(checkpoint_data) + + assert state.checkpoint_data == checkpoint_data + assert state.last_checkpoint is not None + + +# ============================================================================= +# ConnectionManager Tests +# ============================================================================= + +class TestConnectionManager: + """Tests for ConnectionManager class.""" + + def test_singleton_pattern(self, fresh_connection_manager): + """ConnectionManager is a singleton.""" + manager1 = ConnectionManager() + manager2 = ConnectionManager() + + assert manager1 is manager2 + + def test_register_connection(self, fresh_connection_manager, mock_websocket, mock_event_manager): + """Registering a connection stores references.""" + manager = fresh_connection_manager + + manager.register_connection("session-1", mock_websocket, mock_event_manager) + + assert "session-1" in manager._websockets + assert "session-1" in manager._event_managers + assert "session-1" in manager._heartbeat_states + + def test_register_run(self, fresh_connection_manager): + """Registering a run creates RunConnectionState.""" + manager = fresh_connection_manager + + run_state = manager.register_run("run-1", "session-1") + + assert run_state is not None + assert run_state.run_id == "run-1" + assert run_state.session_id == "session-1" + assert run_state.state == ConnectionState.CONNECTED + assert manager.get_run_state("run-1") is run_state + + @pytest.mark.asyncio + async def test_unregister_connection_starts_grace_period( + self, fresh_connection_manager, mock_websocket, mock_event_manager + ): + """Unregistering a connection starts grace period for active runs.""" + manager = fresh_connection_manager + + manager.register_connection("session-1", mock_websocket, mock_event_manager) + manager.register_run("run-1", "session-1") + + runs_in_grace = manager.unregister_connection("session-1") + + assert "run-1" in runs_in_grace + run_state = manager.get_run_state("run-1") + assert run_state.state == ConnectionState.GRACE_PERIOD + + # Clean up the monitor task + if manager._monitor_task: + manager._monitor_task.cancel() + try: + await manager._monitor_task + except asyncio.CancelledError: + pass + + def test_is_run_active_connected(self, fresh_connection_manager): + """Connected runs are active.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + assert manager.is_run_active("run-1") + + def test_is_run_active_grace_period(self, fresh_connection_manager): + """Runs in grace period are active.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + run_state = manager.get_run_state("run-1") + run_state.start_grace_period() + + assert manager.is_run_active("run-1") + + def test_is_run_active_terminated(self, fresh_connection_manager): + """Terminated runs are not active.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + manager.terminate_run("run-1") + + assert not manager.is_run_active("run-1") + + def test_can_reconnect_in_grace(self, fresh_connection_manager): + """Can reconnect during grace period.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + run_state = manager.get_run_state("run-1") + run_state.start_grace_period() + + assert manager.can_reconnect("run-1") + + def test_cannot_reconnect_if_connected(self, fresh_connection_manager): + """Cannot reconnect if already connected.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + assert not manager.can_reconnect("run-1") + + def test_cannot_reconnect_if_expired(self, fresh_connection_manager): + """Cannot reconnect if grace period expired.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + run_state = manager.get_run_state("run-1") + run_state.state = ConnectionState.GRACE_PERIOD + run_state.grace_period_expires = datetime.now() - timedelta(seconds=10) + + assert not manager.can_reconnect("run-1") + + def test_terminate_run_returns_tasks(self, fresh_connection_manager): + """Terminating a run returns its tasks.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + # Add a mock task + mock_task = MagicMock() + run_state = manager.get_run_state("run-1") + run_state.tasks["task-1"] = mock_task + + tasks = manager.terminate_run("run-1") + + assert mock_task in tasks + assert manager.get_run_state("run-1") is None + + def test_should_buffer_event_in_grace(self, fresh_connection_manager): + """Events should be buffered when in grace period.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + run_state = manager.get_run_state("run-1") + run_state.start_grace_period() + + assert manager.should_buffer_event("run-1") + + def test_should_not_buffer_when_connected(self, fresh_connection_manager): + """Events should not be buffered when connected.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + assert not manager.should_buffer_event("run-1") + + def test_buffer_event(self, fresh_connection_manager): + """Events can be buffered via manager.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + manager.buffer_event("run-1", "test_event", {"data": "value"}) + + run_state = manager.get_run_state("run-1") + assert len(run_state.event_buffer) == 1 + + def test_save_checkpoint(self, fresh_connection_manager): + """Checkpoints can be saved via manager.""" + manager = fresh_connection_manager + manager.register_run("run-1", "session-1") + + checkpoint = {"status": "running"} + manager.save_checkpoint("run-1", checkpoint) + + assert manager.get_checkpoint("run-1") == checkpoint + + def test_handle_pong(self, fresh_connection_manager, mock_websocket, mock_event_manager): + """Handling pong resets missed heartbeats.""" + manager = fresh_connection_manager + manager.register_connection("session-1", mock_websocket, mock_event_manager) + + heartbeat_state = manager._heartbeat_states["session-1"] + heartbeat_state.missed_heartbeats = 2 + + manager.handle_pong("session-1") + + assert heartbeat_state.missed_heartbeats == 0 + + def test_get_stats(self, fresh_connection_manager, mock_websocket, mock_event_manager): + """Stats reflect current state.""" + manager = fresh_connection_manager + + manager.register_connection("session-1", mock_websocket, mock_event_manager) + manager.register_run("run-1", "session-1") + manager.register_run("run-2", "session-1") + + # Put one run in grace period + run_state = manager.get_run_state("run-2") + run_state.start_grace_period() + + stats = manager.get_stats() + + assert stats["total_runs"] == 2 + assert stats["connected_runs"] == 1 + assert stats["grace_period_runs"] == 1 + assert stats["active_websockets"] == 1 + + def test_get_runs_in_grace_period(self, fresh_connection_manager): + """Can get list of runs in grace period.""" + manager = fresh_connection_manager + + manager.register_run("run-1", "session-1") + manager.register_run("run-2", "session-1") + + run_state = manager.get_run_state("run-2") + run_state.start_grace_period() + + grace_runs = manager.get_runs_in_grace_period() + + assert "run-2" in grace_runs + assert "run-1" not in grace_runs + + def test_get_active_run_ids(self, fresh_connection_manager): + """Can get all active run IDs.""" + manager = fresh_connection_manager + + manager.register_run("run-1", "session-1") + manager.register_run("run-2", "session-1") + manager.register_run("run-3", "session-1") + + # Put one in grace period + run_state = manager.get_run_state("run-2") + run_state.start_grace_period() + + # Terminate one + manager.terminate_run("run-3") + + active_runs = manager.get_active_run_ids() + + assert "run-1" in active_runs + assert "run-2" in active_runs + assert "run-3" not in active_runs + + +# ============================================================================= +# Reconnection Tests +# ============================================================================= + +class TestReconnection: + """Tests for reconnection functionality.""" + + @pytest.mark.asyncio + async def test_reconnect_run_success( + self, fresh_connection_manager, mock_websocket, mock_event_manager + ): + """Successful reconnection during grace period.""" + manager = fresh_connection_manager + + # Setup initial connection and run + manager.register_connection("session-1", mock_websocket, mock_event_manager) + manager.register_run("run-1", "session-1") + + # Buffer some events + run_state = manager.get_run_state("run-1") + run_state.start_grace_period() + run_state.buffer_event("event1", {"data": "test"}) + + # Create new connection + new_ws = MagicMock() + new_em = MagicMock() + new_em.session_id = "session-2" + new_em.emit_system_event = AsyncMock() + new_em.emit_raw = AsyncMock() + + # Reconnect + result = await manager.reconnect_run("run-1", "session-2", new_ws, new_em) + + assert result is not None + assert result["success"] is True + assert "events_replayed" in result + + # Verify run state was updated + run_state = manager.get_run_state("run-1") + assert run_state.state == ConnectionState.CONNECTED + assert run_state.session_id == "session-2" + + # Events should have been replayed + assert new_em.emit_system_event.called + assert new_em.emit_raw.called + + @pytest.mark.asyncio + async def test_reconnect_run_not_found(self, fresh_connection_manager, mock_websocket, mock_event_manager): + """Reconnection fails if run doesn't exist.""" + manager = fresh_connection_manager + + result = await manager.reconnect_run("nonexistent", "session-1", mock_websocket, mock_event_manager) + + assert result is not None + assert result["success"] is False + assert "not found" in result["error"] + + @pytest.mark.asyncio + async def test_reconnect_run_not_in_grace( + self, fresh_connection_manager, mock_websocket, mock_event_manager + ): + """Reconnection fails if not in grace period.""" + manager = fresh_connection_manager + + manager.register_connection("session-1", mock_websocket, mock_event_manager) + manager.register_run("run-1", "session-1") + + # Try to reconnect while still connected + new_ws = MagicMock() + new_em = MagicMock() + new_em.session_id = "session-2" + + result = await manager.reconnect_run("run-1", "session-2", new_ws, new_em) + + assert result is not None + assert result["success"] is False + assert "not in reconnectable state" in result["error"] + + +# ============================================================================= +# Configuration Tests +# ============================================================================= + +class TestConnectionConfig: + """Tests for configuration values.""" + + def test_default_heartbeat_interval(self): + """Default heartbeat interval is 30 seconds.""" + assert ConnectionConfig.HEARTBEAT_INTERVAL_SECONDS == 30.0 + + def test_default_grace_period(self): + """Default grace period is 120 seconds.""" + assert ConnectionConfig.RECONNECTION_GRACE_PERIOD_SECONDS == 120.0 + + def test_default_max_missed_heartbeats(self): + """Default max missed heartbeats is 3.""" + assert ConnectionConfig.MAX_MISSED_HEARTBEATS == 3 + + def test_default_max_buffered_events(self): + """Default max buffered events is 1000.""" + assert ConnectionConfig.MAX_BUFFERED_EVENTS == 1000 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/core/tests/test_content_selection.py b/core/tests/test_content_selection.py new file mode 100644 index 0000000..893222c --- /dev/null +++ b/core/tests/test_content_selection.py @@ -0,0 +1,604 @@ +""" +Tests for content_selection.py - Budget-aware content inheritance utilities. + +These tests cover the two-tier content selection strategy used to prevent +Associates from being "born over-budget" when inheriting context from +completed work modules. + +Test Coverage: +1. Budget computation (compute_inheritance_budget_chars) +2. Message size estimation (_estimate_message_chars) +3. Newest-first message selection (_select_messages_newest_first) +4. Two-tier selection (select_content_within_budget) +5. Briefing formatting (format_inherited_content_for_briefing) +6. Async hydration helpers +7. Edge cases and error handling +""" +import pytest +import sys +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock + +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + +from agent_core.utils.content_selection import ( + # Constants + CHARS_PER_TOKEN, + INHERITANCE_BUDGET_FRACTION, + STRATEGY_LLM_SUMMARY, + STRATEGY_NEWEST_FIRST, + STRATEGY_EMPTY, + # Functions + compute_inheritance_budget_chars, + _estimate_message_chars, + _select_messages_newest_first, + select_content_within_budget, + format_inherited_content_for_briefing, + hydrate_messages_for_selection, + select_inherited_content_with_hydration, +) + + +class TestConstants: + """Tests for module-level constants.""" + + def test_chars_per_token_is_reasonable(self): + """CHARS_PER_TOKEN should be a reasonable approximation.""" + assert CHARS_PER_TOKEN == 4 + assert isinstance(CHARS_PER_TOKEN, int) + + def test_inheritance_budget_fraction_is_reasonable(self): + """INHERITANCE_BUDGET_FRACTION should leave room for agent's own work.""" + assert INHERITANCE_BUDGET_FRACTION == 0.40 + assert 0 < INHERITANCE_BUDGET_FRACTION < 0.5 # Less than half + + def test_strategy_constants_are_strings(self): + """Strategy constants should be strings.""" + assert isinstance(STRATEGY_LLM_SUMMARY, str) + assert isinstance(STRATEGY_NEWEST_FIRST, str) + assert isinstance(STRATEGY_EMPTY, str) + + def test_strategy_constants_are_unique(self): + """Strategy constants should be distinct values.""" + strategies = {STRATEGY_LLM_SUMMARY, STRATEGY_NEWEST_FIRST, STRATEGY_EMPTY} + assert len(strategies) == 3 + + +class TestComputeInheritanceBudgetChars: + """Tests for compute_inheritance_budget_chars function.""" + + def test_basic_computation(self): + """Test basic budget computation with typical values.""" + # 200K tokens, 2 sources + result = compute_inheritance_budget_chars(200000, 2) + # (200000 * 0.40 / 2) * 4 = 160000 + assert result == 160000 + + def test_single_source(self): + """Single source should get full inheritance pool.""" + result = compute_inheritance_budget_chars(200000, 1) + # (200000 * 0.40 / 1) * 4 = 320000 + assert result == 320000 + + def test_many_sources(self): + """Many sources should split budget evenly.""" + result = compute_inheritance_budget_chars(200000, 5) + # (200000 * 0.40 / 5) * 4 = 64000 + assert result == 64000 + + def test_1m_context(self): + """Test with 1M context limit.""" + result = compute_inheritance_budget_chars(1000000, 4) + # (1000000 * 0.40 / 4) * 4 = 400000 + assert result == 400000 + + def test_custom_fraction(self): + """Test with custom inheritance fraction.""" + result = compute_inheritance_budget_chars(200000, 2, inheritance_fraction=0.20) + # (200000 * 0.20 / 2) * 4 = 80000 + assert result == 80000 + + def test_zero_sources_returns_zero(self): + """Zero sources should return zero budget.""" + result = compute_inheritance_budget_chars(200000, 0) + assert result == 0 + + def test_negative_sources_returns_zero(self): + """Negative sources should return zero budget.""" + result = compute_inheritance_budget_chars(200000, -1) + assert result == 0 + + def test_result_is_integer(self): + """Result should be an integer (floor division).""" + result = compute_inheritance_budget_chars(100000, 3) + assert isinstance(result, int) + + +class TestEstimateMessageChars: + """Tests for _estimate_message_chars function.""" + + def test_simple_content(self): + """Test with simple string content.""" + msg = {"role": "assistant", "content": "Hello world"} + result = _estimate_message_chars(msg) + # len("Hello world") = 11, plus ~50 overhead + assert result >= 11 + assert result < 100 + + def test_empty_content(self): + """Test with empty content.""" + msg = {"role": "assistant", "content": ""} + result = _estimate_message_chars(msg) + # Should still have overhead + assert result >= 50 + + def test_no_content_field(self): + """Test message without content field.""" + msg = {"role": "assistant"} + result = _estimate_message_chars(msg) + # Should return overhead only + assert result >= 50 + + def test_tool_calls(self): + """Test message with tool calls.""" + msg = { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": "search_web", + "arguments": '{"query": "python testing"}' + } + } + ] + } + result = _estimate_message_chars(msg) + # Should include tool call overhead + assert result > 50 + + def test_multimodal_content(self): + """Test with list content (multimodal).""" + msg = { + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image"}, + {"type": "image_url", "image_url": {"url": "..."}} + ] + } + result = _estimate_message_chars(msg) + assert result > 50 + + def test_large_content(self): + """Test with large content.""" + large_text = "x" * 10000 + msg = {"role": "assistant", "content": large_text} + result = _estimate_message_chars(msg) + assert result >= 10000 + + +class TestSelectMessagesNewestFirst: + """Tests for _select_messages_newest_first function.""" + + @pytest.fixture + def sample_messages(self): + """Create sample messages of varying sizes.""" + return [ + {"role": "user", "content": "Message 1 - 100 chars" + "x" * 80}, # ~100 chars + overhead + {"role": "assistant", "content": "Message 2 - 200 chars" + "x" * 180}, # ~200 chars + overhead + {"role": "user", "content": "Message 3 - 150 chars" + "x" * 130}, # ~150 chars + overhead + {"role": "assistant", "content": "Message 4 - 300 chars" + "x" * 280}, # ~300 chars + overhead + {"role": "user", "content": "Message 5 - 50 chars" + "x" * 30}, # ~50 chars + overhead + ] + + def test_selects_newest_first(self, sample_messages): + """Should select messages from newest to oldest.""" + selected, metadata = _select_messages_newest_first( + sample_messages, + budget_chars=500, + source_id="test" + ) + # Should include message 5 (newest) first + assert len(selected) > 0 + # First message in selected should be one of the original ones + assert selected[-1] == sample_messages[-1] or selected[-1] == sample_messages[-2] + + def test_respects_budget(self, sample_messages): + """Should stop when budget is exhausted.""" + selected, metadata = _select_messages_newest_first( + sample_messages, + budget_chars=200, # Very tight budget + source_id="test" + ) + # Should not include all messages + assert len(selected) < len(sample_messages) + + def test_always_includes_at_least_one(self, sample_messages): + """Should always include at least one message even if over budget.""" + selected, metadata = _select_messages_newest_first( + sample_messages, + budget_chars=10, # Impossibly small budget + source_id="test" + ) + assert len(selected) >= 1 + + def test_preserves_original_order(self, sample_messages): + """Selected messages should be in original order.""" + selected, metadata = _select_messages_newest_first( + sample_messages, + budget_chars=10000, # Large budget + source_id="test" + ) + # Should be in original order + original_indices = [sample_messages.index(msg) for msg in selected] + assert original_indices == sorted(original_indices) + + def test_empty_messages(self): + """Should handle empty message list.""" + selected, metadata = _select_messages_newest_first([], budget_chars=1000, source_id="test") + assert selected == [] + assert metadata["strategy"] == STRATEGY_EMPTY + assert metadata["items_selected"] == 0 + + def test_metadata_correctness(self, sample_messages): + """Should return accurate metadata.""" + selected, metadata = _select_messages_newest_first( + sample_messages, + budget_chars=500, + source_id="test_source" + ) + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + assert metadata["source_id"] == "test_source" + assert metadata["items_available"] == len(sample_messages) + assert metadata["items_selected"] == len(selected) + assert "chars_used" in metadata + assert "budget_chars" in metadata + + +class TestSelectContentWithinBudget: + """Tests for select_content_within_budget function.""" + + @pytest.fixture + def sample_deliverables(self): + """Sample deliverables with primary_summary.""" + return { + "primary_summary": "This is a 50 char summary for testing purposes.", + "other_data": {"key": "value"} + } + + @pytest.fixture + def sample_messages(self): + """Sample messages for fallback selection.""" + return [ + {"role": "user", "content": "First message with some content."}, + {"role": "assistant", "content": "Response with detailed information about the topic."}, + {"role": "user", "content": "Follow-up question about specific details."}, + ] + + def test_tier1_uses_summary_when_fits(self, sample_deliverables, sample_messages): + """Tier 1: Should use summary when it fits budget.""" + selected, metadata = select_content_within_budget( + deliverables=sample_deliverables, + messages=sample_messages, + budget_chars=1000, # Large enough for summary + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_LLM_SUMMARY + assert isinstance(selected, str) + assert selected == sample_deliverables["primary_summary"] + + def test_tier2_fallback_when_summary_too_large(self, sample_deliverables, sample_messages): + """Tier 2: Should fall back to messages when summary exceeds budget.""" + selected, metadata = select_content_within_budget( + deliverables=sample_deliverables, + messages=sample_messages, + budget_chars=20, # Too small for summary + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + assert isinstance(selected, list) + + def test_tier2_when_no_summary(self, sample_messages): + """Tier 2: Should use messages when no summary exists.""" + selected, metadata = select_content_within_budget( + deliverables={}, # No primary_summary + messages=sample_messages, + budget_chars=1000, + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + + def test_tier2_when_deliverables_none(self, sample_messages): + """Tier 2: Should use messages when deliverables is None.""" + selected, metadata = select_content_within_budget( + deliverables=None, + messages=sample_messages, + budget_chars=1000, + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + + def test_empty_when_no_content(self): + """Should return empty strategy when nothing available.""" + selected, metadata = select_content_within_budget( + deliverables={}, + messages=[], + budget_chars=1000, + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_EMPTY + assert selected == [] + + def test_metadata_includes_budget_info(self, sample_deliverables, sample_messages): + """Metadata should include budget information.""" + _, metadata = select_content_within_budget( + deliverables=sample_deliverables, + messages=sample_messages, + budget_chars=1000, + source_id="test_source" + ) + assert "chars_used" in metadata + assert "source_id" in metadata + assert metadata["source_id"] == "test_source" + + +class TestFormatInheritedContentForBriefing: + """Tests for format_inherited_content_for_briefing function.""" + + def test_format_llm_summary(self): + """Should format LLM summary into single message.""" + content = "This is the summary content." + metadata = {"strategy": STRATEGY_LLM_SUMMARY, "chars_used": 100} + + result = format_inherited_content_for_briefing(content, metadata, "WM_1") + + assert len(result) == 1 + assert result[0]["role"] == "user" + assert "WM_1" in result[0]["content"] + assert "summary content" in result[0]["content"] + assert result[0]["_internal"]["_no_handover"] is True + assert result[0]["_internal"]["_inherited_from"] == "WM_1" + + def test_format_newest_first_messages(self): + """Should format message list with header.""" + messages = [ + {"role": "user", "content": "Message 1"}, + {"role": "assistant", "content": "Message 2"}, + ] + metadata = { + "strategy": STRATEGY_NEWEST_FIRST, + "chars_used": 200, + "items_selected": 2 + } + + result = format_inherited_content_for_briefing(messages, metadata, "WM_2") + + # Header + 2 messages = 3 items + assert len(result) == 3 + # First should be header + assert "[Context inherited from WM_2" in result[0]["content"] + assert result[0]["_internal"]["_is_header"] is True + # Rest should be messages with inheritance metadata + assert result[1]["_internal"]["_inherited_from"] == "WM_2" + assert result[2]["_internal"]["_inherited_from"] == "WM_2" + + def test_format_empty_returns_empty(self): + """Should return empty list for empty strategy.""" + result = format_inherited_content_for_briefing( + [], + {"strategy": STRATEGY_EMPTY}, + "WM_1" + ) + assert result == [] + + def test_all_messages_marked_no_handover(self): + """All formatted messages should have _no_handover flag.""" + messages = [{"role": "user", "content": "Test"}] + metadata = {"strategy": STRATEGY_NEWEST_FIRST, "chars_used": 50} + + result = format_inherited_content_for_briefing(messages, metadata, "WM_1") + + for msg in result: + assert msg.get("_internal", {}).get("_no_handover") is True + + +class TestHydrateMessagesForSelection: + """Tests for hydrate_messages_for_selection async function.""" + + @pytest.mark.asyncio + async def test_hydrates_messages(self): + """Should hydrate messages using knowledge base.""" + messages = [ + {"role": "user", "content": "Check <#CGKB-00001> for details"}, + {"role": "assistant", "content": "Found information in <#CGKB-00002>"}, + ] + + mock_kb = MagicMock() + mock_kb.hydrate_content = AsyncMock(side_effect=lambda c: c.replace( + "<#CGKB-00001>", "HYDRATED_CONTENT_1" + ).replace( + "<#CGKB-00002>", "HYDRATED_CONTENT_2" + )) + + result = await hydrate_messages_for_selection(messages, mock_kb, "test") + + assert "HYDRATED_CONTENT_1" in result[0]["content"] + assert "HYDRATED_CONTENT_2" in result[1]["content"] + + @pytest.mark.asyncio + async def test_handles_missing_kb(self): + """Should return original messages when KB is None.""" + messages = [{"role": "user", "content": "Test <#CGKB-00001>"}] + + result = await hydrate_messages_for_selection(messages, None, "test") + + assert result == messages + assert "<#CGKB-00001>" in result[0]["content"] + + @pytest.mark.asyncio + async def test_handles_empty_messages(self): + """Should handle empty message list.""" + mock_kb = MagicMock() + + result = await hydrate_messages_for_selection([], mock_kb, "test") + + assert result == [] + + @pytest.mark.asyncio + async def test_handles_hydration_error(self): + """Should keep original content on hydration error.""" + messages = [{"role": "user", "content": "Test <#CGKB-00001>"}] + + mock_kb = MagicMock() + mock_kb.hydrate_content = AsyncMock(side_effect=Exception("KB error")) + + result = await hydrate_messages_for_selection(messages, mock_kb, "test") + + # Should have original content + assert result[0]["content"] == messages[0]["content"] + + +class TestSelectInheritedContentWithHydration: + """Tests for select_inherited_content_with_hydration async function.""" + + @pytest.fixture + def sample_archive_entry(self): + """Sample context archive entry.""" + return { + "messages": [ + {"role": "user", "content": "Query about <#CGKB-00001>"}, + {"role": "assistant", "content": "Response with details"}, + ], + "deliverables": { + "primary_summary": "Summary of the work done." + } + } + + @pytest.mark.asyncio + async def test_uses_tier1_when_summary_fits(self, sample_archive_entry): + """Should use Tier 1 (summary) when it fits budget.""" + mock_kb = MagicMock() + mock_kb.hydrate_content = AsyncMock(side_effect=lambda c: c) + + selected, metadata = await select_inherited_content_with_hydration( + context_archive_entry=sample_archive_entry, + budget_chars=1000, + knowledge_base=mock_kb, + source_id="WM_1" + ) + + assert metadata["strategy"] == STRATEGY_LLM_SUMMARY + assert selected == sample_archive_entry["deliverables"]["primary_summary"] + + @pytest.mark.asyncio + async def test_uses_tier2_when_no_summary(self): + """Should use Tier 2 (messages) when no summary available.""" + archive_entry = { + "messages": [ + {"role": "user", "content": "Test message"}, + ], + "deliverables": {} # No summary + } + + mock_kb = MagicMock() + mock_kb.hydrate_content = AsyncMock(side_effect=lambda c: c) + + selected, metadata = await select_inherited_content_with_hydration( + context_archive_entry=archive_entry, + budget_chars=1000, + knowledge_base=mock_kb, + source_id="WM_1" + ) + + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + + @pytest.mark.asyncio + async def test_hydrates_before_selection(self): + """Should hydrate messages before selection for accurate sizing.""" + # Message with KB token that expands significantly + archive_entry = { + "messages": [ + {"role": "user", "content": "<#CGKB-00001>"}, # Short when dehydrated + ], + "deliverables": {} + } + + # Simulate KB token expanding to large content + large_content = "x" * 50000 # 50K chars + mock_kb = MagicMock() + mock_kb.hydrate_content = AsyncMock(return_value=large_content) + + selected, metadata = await select_inherited_content_with_hydration( + context_archive_entry=archive_entry, + budget_chars=1000, # Small budget + knowledge_base=mock_kb, + source_id="WM_1" + ) + + # Should have hydrated - KB method called + mock_kb.hydrate_content.assert_called() + + # Should report chars_used based on hydrated size + if metadata["strategy"] == STRATEGY_NEWEST_FIRST: + assert metadata["chars_used"] >= 1000 or len(selected) == 1 # At least one message + + +class TestEdgeCases: + """Tests for edge cases and error handling.""" + + def test_non_string_summary_ignored(self): + """Non-string summary should fall back to Tier 2.""" + selected, metadata = select_content_within_budget( + deliverables={"primary_summary": 12345}, # Not a string + messages=[{"role": "user", "content": "Test"}], + budget_chars=1000, + source_id="test" + ) + assert metadata["strategy"] == STRATEGY_NEWEST_FIRST + + def test_unicode_content_handling(self): + """Should handle unicode content correctly.""" + messages = [{"role": "user", "content": "γ“γ‚“γ«γ‘γ―δΈ–η•Œ 🌍"}] + selected, metadata = select_content_within_budget( + deliverables={}, + messages=messages, + budget_chars=1000, + source_id="test" + ) + assert len(selected) > 0 + + def test_nested_content_in_messages(self): + """Should handle nested content structures.""" + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Part 1"}, + {"type": "text", "text": "Part 2"}, + ] + } + ] + result = _estimate_message_chars(messages[0]) + assert result > 0 + + def test_very_large_budget(self): + """Should handle very large budgets without issues.""" + messages = [{"role": "user", "content": "Small message"}] + selected, metadata = select_content_within_budget( + deliverables={}, + messages=messages, + budget_chars=10000000, # 10M chars + source_id="test" + ) + assert len(selected) == len(messages) + + def test_zero_budget(self): + """Should still return at least one message with zero budget.""" + messages = [{"role": "user", "content": "Test"}] + selected, metadata = _select_messages_newest_first( + messages, + budget_chars=0, + source_id="test" + ) + assert len(selected) >= 1 # At least one diff --git a/core/tests/test_context_budget_guardian.py b/core/tests/test_context_budget_guardian.py new file mode 100644 index 0000000..298af8d --- /dev/null +++ b/core/tests/test_context_budget_guardian.py @@ -0,0 +1,368 @@ +""" +Tests for context_budget_guardian.py - budget calculations and threshold logic. + +These tests cover the new Context Budget Guardian that provides proactive +context window monitoring to prevent agents from hitting ContextWindowExceededError. +""" +import pytest +import sys +from pathlib import Path + +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + +from agent_core.framework.context_budget_guardian import ( + ContextBudgetStatus, + DEFAULT_CONTEXT_LIMITS, + WARNING_THRESHOLD, + CRITICAL_THRESHOLD, + EXCEEDED_THRESHOLD, + get_model_context_limit, + calculate_worker_budget, + assess_context_budget, + generate_context_budget_directive, +) + + +class TestContextBudgetThresholds: + """Tests for threshold constants.""" + + def test_thresholds_are_ascending(self): + """Thresholds should be in ascending order.""" + assert WARNING_THRESHOLD < CRITICAL_THRESHOLD < EXCEEDED_THRESHOLD + + def test_thresholds_leave_headroom(self): + """EXCEEDED threshold should leave at least 30% headroom.""" + assert EXCEEDED_THRESHOLD <= 0.70 + + def test_warning_threshold_value(self): + """WARNING should trigger at 40%.""" + assert WARNING_THRESHOLD == 0.40 + + def test_critical_threshold_value(self): + """CRITICAL should trigger at 55%.""" + assert CRITICAL_THRESHOLD == 0.55 + + def test_exceeded_threshold_value(self): + """EXCEEDED should trigger at 70%.""" + assert EXCEEDED_THRESHOLD == 0.70 + + +class TestGetModelContextLimit: + """Tests for get_model_context_limit function.""" + + def test_explicit_config_takes_priority(self): + """Explicit max_context_tokens in config should override defaults.""" + config = {"max_context_tokens": 500000} + result = get_model_context_limit("any-model", config) + assert result == 500000 + + def test_exact_model_match(self): + """Exact model name should match.""" + result = get_model_context_limit("openai/gpt-4o") + assert result == 128000 + + def test_model_prefix_match(self): + """Versioned model names should match by prefix.""" + # claude-sonnet-4-20250514 should match anthropic/claude-sonnet-4 + result = get_model_context_limit("claude-sonnet-4-20250514") + assert result == 200000 + + def test_anthropic_claude_models(self): + """Various Claude model formats should be detected.""" + models = [ + "anthropic/claude-sonnet-4", + "claude-sonnet-4-20250514", + "anthropic/claude-3-5-sonnet", + "claude-3-5-sonnet-20241022", + ] + for model in models: + result = get_model_context_limit(model) + assert result == 200000, f"Failed for model: {model}" + + def test_openai_gpt4o_models(self): + """GPT-4o models should return 128K.""" + models = ["openai/gpt-4o", "gpt-4o", "gpt-4o-2024-05-13"] + for model in models: + result = get_model_context_limit(model) + assert result == 128000, f"Failed for model: {model}" + + def test_gemini_models(self): + """Gemini models should return 1M.""" + result = get_model_context_limit("gemini/gemini-2.5-pro") + assert result == 1000000 + + def test_unknown_model_returns_conservative_default(self): + """Unknown models should return conservative 100K default.""" + result = get_model_context_limit("unknown/mystery-model-v99") + assert result == 100000 + + def test_1m_context_with_header(self): + """Models with anthropic-beta header should get 1M context.""" + config = { + "extra_headers": { + "anthropic-beta": "context-1m-2025-01-01" + } + } + result = get_model_context_limit("anthropic/claude-sonnet-4-5", config) + assert result == 1000000 + + def test_1m_context_requires_supporting_model(self): + """1M context header should only work for supporting models.""" + config = { + "extra_headers": { + "anthropic-beta": "context-1m-2025-01-01" + } + } + # GPT-4 doesn't support 1M even with the header + result = get_model_context_limit("openai/gpt-4o", config) + assert result == 128000 + + +class TestCalculateWorkerBudget: + """Tests for calculate_worker_budget function.""" + + def test_basic_calculation(self): + """Basic budget calculation with default settings.""" + result = calculate_worker_budget( + model_context_limit=200000, + num_workers=3 + ) + + # Total available = 200000 - 30000 overhead = 170000 + assert result["total_available"] == 170000 + + # Summarization = 30% of 170000 = 51000 + assert result["summarization_budget"] == 51000 + + # Worker total = 170000 - 51000 = 119000 + assert result["worker_budget_total"] == 119000 + + # Per worker = 119000 / 3 = 39666 + assert result["per_worker_budget"] == 39666 + + def test_single_worker(self): + """Single worker should get full worker budget.""" + result = calculate_worker_budget( + model_context_limit=200000, + num_workers=1 + ) + + assert result["per_worker_budget"] == result["worker_budget_total"] + + def test_zero_workers_handled(self): + """Zero workers should not cause division by zero.""" + result = calculate_worker_budget( + model_context_limit=200000, + num_workers=0 + ) + + # Should get full worker budget (no division) + assert result["per_worker_budget"] == result["worker_budget_total"] + + def test_custom_overhead(self): + """Custom base_context_overhead should be respected.""" + result = calculate_worker_budget( + model_context_limit=200000, + num_workers=2, + base_context_overhead=50000 + ) + + assert result["total_available"] == 150000 + + def test_custom_summarization_reserve(self): + """Custom summarization_reserve should be respected.""" + result = calculate_worker_budget( + model_context_limit=200000, + num_workers=2, + summarization_reserve=0.50 # 50% reserve + ) + + # With 50% reserve, summarization gets half + assert result["summarization_budget"] == int(170000 * 0.50) + + def test_large_context_model(self): + """1M context model should scale appropriately.""" + result = calculate_worker_budget( + model_context_limit=1000000, + num_workers=5 + ) + + # With 1M context, workers get much more budget + assert result["per_worker_budget"] > 100000 + + +class TestAssessContextBudget: + """Tests for assess_context_budget function.""" + + def test_healthy_status(self): + """Under 40% should be HEALTHY.""" + status, metadata = assess_context_budget( + predicted_tokens=50000, # 25% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.HEALTHY + assert metadata["utilization_percent"] == 25.0 + assert "Healthy" in metadata["recommendation"] + + def test_warning_status(self): + """40-55% should be WARNING.""" + status, metadata = assess_context_budget( + predicted_tokens=90000, # 45% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.WARNING + assert metadata["utilization_percent"] == 45.0 + assert "WARNING" in metadata["recommendation"] + + def test_critical_status(self): + """55-70% should be CRITICAL.""" + status, metadata = assess_context_budget( + predicted_tokens=120000, # 60% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.CRITICAL + assert metadata["utilization_percent"] == 60.0 + assert "CRITICAL" in metadata["recommendation"] + + def test_exceeded_status(self): + """Over 70% should be EXCEEDED.""" + status, metadata = assess_context_budget( + predicted_tokens=150000, # 75% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.EXCEEDED + assert metadata["utilization_percent"] == 75.0 + assert "EMERGENCY" in metadata["recommendation"] + + def test_metadata_includes_all_fields(self): + """Metadata should include all expected fields.""" + status, metadata = assess_context_budget( + predicted_tokens=50000, + model_name="anthropic/claude-sonnet-4", + agent_id="test_agent" + ) + + assert "context_limit" in metadata + assert "predicted_tokens" in metadata + assert "utilization_percent" in metadata + assert "remaining_tokens" in metadata + assert "model_name" in metadata + assert "agent_id" in metadata + assert "recommendation" in metadata + + def test_remaining_tokens_calculation(self): + """remaining_tokens should be calculated correctly.""" + status, metadata = assess_context_budget( + predicted_tokens=50000, + model_name="anthropic/claude-sonnet-4" # 200K limit + ) + + assert metadata["remaining_tokens"] == 150000 + + def test_boundary_at_warning_threshold(self): + """Exactly at 40% should be WARNING.""" + status, _ = assess_context_budget( + predicted_tokens=80000, # 40% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.WARNING + + def test_just_under_warning_threshold(self): + """Just under 40% should be HEALTHY.""" + status, _ = assess_context_budget( + predicted_tokens=79000, # 39.5% of 200K + model_name="anthropic/claude-sonnet-4" + ) + + assert status == ContextBudgetStatus.HEALTHY + + +class TestGenerateContextBudgetDirective: + """Tests for generate_context_budget_directive function.""" + + def test_healthy_returns_none(self): + """HEALTHY status should return None (no directive needed).""" + result = generate_context_budget_directive( + ContextBudgetStatus.HEALTHY, + {"utilization_percent": 30, "remaining_tokens": 140000} + ) + + assert result is None + + def test_warning_returns_directive(self): + """WARNING status should return a directive.""" + result = generate_context_budget_directive( + ContextBudgetStatus.WARNING, + {"utilization_percent": 45, "remaining_tokens": 110000} + ) + + assert result is not None + assert "WARNING" in result + assert "45%" in result + + def test_critical_returns_directive(self): + """CRITICAL status should return a directive.""" + result = generate_context_budget_directive( + ContextBudgetStatus.CRITICAL, + {"utilization_percent": 60, "remaining_tokens": 80000} + ) + + assert result is not None + assert "CRITICAL" in result + + def test_exceeded_returns_directive(self): + """EXCEEDED status should return a directive.""" + result = generate_context_budget_directive( + ContextBudgetStatus.EXCEEDED, + {"utilization_percent": 75, "remaining_tokens": 50000} + ) + + assert result is not None + assert "EMERGENCY" in result or "EXCEEDED" in result + + def test_principal_agent_uses_finish_flow(self): + """Principal agent directive should mention finish_flow.""" + result = generate_context_budget_directive( + ContextBudgetStatus.WARNING, + {"utilization_percent": 45, "remaining_tokens": 110000}, + agent_type="principal" + ) + + assert "finish_flow" in result + + def test_associate_agent_uses_generate_message_summary(self): + """Associate agent directive should mention generate_message_summary.""" + result = generate_context_budget_directive( + ContextBudgetStatus.WARNING, + {"utilization_percent": 45, "remaining_tokens": 110000}, + agent_type="associate" + ) + + assert "generate_message_summary" in result + + +class TestContextBudgetStatusEnum: + """Tests for the ContextBudgetStatus enum.""" + + def test_all_statuses_defined(self): + """All expected statuses should be defined.""" + assert hasattr(ContextBudgetStatus, 'HEALTHY') + assert hasattr(ContextBudgetStatus, 'WARNING') + assert hasattr(ContextBudgetStatus, 'CRITICAL') + assert hasattr(ContextBudgetStatus, 'EXCEEDED') + + def test_statuses_are_unique(self): + """Each status should have a unique value.""" + values = [ + ContextBudgetStatus.HEALTHY.value, + ContextBudgetStatus.WARNING.value, + ContextBudgetStatus.CRITICAL.value, + ContextBudgetStatus.EXCEEDED.value, + ] + assert len(values) == len(set(values)) diff --git a/core/tests/test_context_helpers.py b/core/tests/test_context_helpers.py new file mode 100644 index 0000000..9b6c1e7 --- /dev/null +++ b/core/tests/test_context_helpers.py @@ -0,0 +1,419 @@ +""" +Unit tests for agent_core.utils.context_helpers module. + +This module tests the V-Model path resolution system that provides +safe, declarative access to nested context values using dot-notation paths. + +Key concepts tested: +- PATH_RESOLVER_MAP: Maps prefixes (state, meta, team, etc.) to base objects +- _traverse_path: Greedy path traversal with compound key support +- get_nested_value_from_context: Main API for V-Model path resolution +- VModelAccessor: Syntactic sugar class for eval() environments +- SENTINEL_DEFAULT: Distinguishes "not found" from None values +""" + +import pytest +from agent_core.utils.context_helpers import ( + get_nested_value_from_context, + VModelAccessor, + _traverse_path, + PATH_RESOLVER_MAP, + SENTINEL_DEFAULT, + DEFAULT_PATH_PREFIX, +) + + +class TestPathResolverMap: + """Tests for the PATH_RESOLVER_MAP configuration.""" + + def test_contains_expected_prefixes(self): + """Verify all documented prefixes exist in the resolver map.""" + expected_prefixes = [ + "state", "meta", "team", "run", "config", + "initial_params", "flags", "principal", "partner", "_self" + ] + for prefix in expected_prefixes: + assert prefix in PATH_RESOLVER_MAP, f"Missing prefix: {prefix}" + + def test_state_resolver(self): + """Test the 'state' prefix resolves to ctx['state'].""" + ctx = {"state": {"key": "value"}} + resolver = PATH_RESOLVER_MAP["state"] + assert resolver(ctx) == {"key": "value"} + + def test_meta_resolver(self): + """Test the 'meta' prefix resolves to ctx['meta'].""" + ctx = {"meta": {"agent_id": "test-agent"}} + resolver = PATH_RESOLVER_MAP["meta"] + assert resolver(ctx) == {"agent_id": "test-agent"} + + def test_team_resolver(self): + """Test the 'team' prefix resolves to ctx['refs']['team'].""" + ctx = {"refs": {"team": {"question": "What is AI?"}}} + resolver = PATH_RESOLVER_MAP["team"] + assert resolver(ctx) == {"question": "What is AI?"} + + def test_run_resolver(self): + """Test the 'run' prefix resolves to ctx['refs']['run']['meta'].""" + ctx = {"refs": {"run": {"meta": {"run_id": "run-123"}}}} + resolver = PATH_RESOLVER_MAP["run"] + assert resolver(ctx) == {"run_id": "run-123"} + + def test_config_resolver(self): + """Test the 'config' prefix resolves to ctx['refs']['run']['config'].""" + ctx = {"refs": {"run": {"config": {"setting": True}}}} + resolver = PATH_RESOLVER_MAP["config"] + assert resolver(ctx) == {"setting": True} + + def test_initial_params_shortcut(self): + """Test 'initial_params' shortcut to state.initial_parameters.""" + ctx = {"state": {"initial_parameters": {"prompt": "Hello"}}} + resolver = PATH_RESOLVER_MAP["initial_params"] + assert resolver(ctx) == {"prompt": "Hello"} + + def test_flags_shortcut(self): + """Test 'flags' shortcut to state.flags.""" + ctx = {"state": {"flags": {"debug": True}}} + resolver = PATH_RESOLVER_MAP["flags"] + assert resolver(ctx) == {"debug": True} + + def test_self_resolver(self): + """Test '_self' prefix returns the entire context.""" + ctx = {"state": {"foo": "bar"}, "meta": {"id": "123"}} + resolver = PATH_RESOLVER_MAP["_self"] + assert resolver(ctx) is ctx + + def test_principal_cross_context_shortcut(self): + """Test 'principal' resolves to partner's principal_context_ref.state.""" + principal_state = {"messages": [], "deliverables": {}} + ctx = { + "refs": { + "run": { + "sub_context_refs": { + "_principal_context_ref": {"state": principal_state} + } + } + } + } + resolver = PATH_RESOLVER_MAP["principal"] + assert resolver(ctx) == principal_state + + def test_partner_cross_context_shortcut(self): + """Test 'partner' resolves to principal's partner_context_ref.state.""" + partner_state = {"inbox": [], "flags": {}} + ctx = { + "refs": { + "run": { + "sub_context_refs": { + "_partner_context_ref": {"state": partner_state} + } + } + } + } + resolver = PATH_RESOLVER_MAP["partner"] + assert resolver(ctx) == partner_state + + +class TestTraversePath: + """Tests for the _traverse_path helper function.""" + + def test_simple_key_traversal(self): + """Test traversing a single key.""" + obj = {"foo": "bar"} + assert _traverse_path(obj, ["foo"]) == "bar" + + def test_nested_key_traversal(self): + """Test traversing multiple nested keys.""" + obj = {"a": {"b": {"c": "deep_value"}}} + assert _traverse_path(obj, ["a", "b", "c"]) == "deep_value" + + def test_list_index_access(self): + """Test accessing list elements by index.""" + obj = {"items": ["first", "second", "third"]} + assert _traverse_path(obj, ["items", "0"]) == "first" + assert _traverse_path(obj, ["items", "2"]) == "third" + + def test_negative_list_index(self): + """Test negative index access (Python-style).""" + obj = {"items": ["first", "second", "third"]} + assert _traverse_path(obj, ["items[-1]"]) == "third" + assert _traverse_path(obj, ["items[-2]"]) == "second" + + def test_compound_key_with_dots(self): + """Test handling keys that contain dots (greedy matching).""" + obj = {"dotted.key.name": "compound_value", "dotted": {"key": {"name": "nested_value"}}} + # Greedy matching should prefer the compound key + result = _traverse_path(obj, ["dotted.key.name"]) + assert result == "compound_value" + + def test_missing_key_returns_sentinel(self): + """Test that missing keys return SENTINEL_DEFAULT.""" + obj = {"foo": "bar"} + result = _traverse_path(obj, ["nonexistent"]) + assert result is SENTINEL_DEFAULT + + def test_none_in_path_returns_sentinel(self): + """Test that None values in path return SENTINEL_DEFAULT.""" + obj = {"a": None} + result = _traverse_path(obj, ["a", "b"]) + assert result is SENTINEL_DEFAULT + + def test_index_out_of_bounds_returns_sentinel(self): + """Test that out-of-bounds list indices return SENTINEL_DEFAULT.""" + obj = {"items": ["only_one"]} + result = _traverse_path(obj, ["items", "5"]) + assert result is SENTINEL_DEFAULT + + def test_object_attribute_access(self): + """Test accessing object attributes via getattr.""" + class TestObj: + attr = "attr_value" + + obj = {"nested": TestObj()} + result = _traverse_path(obj, ["nested", "attr"]) + assert result == "attr_value" + + def test_empty_path_returns_base(self): + """Test that empty path returns the base object.""" + obj = {"foo": "bar"} + result = _traverse_path(obj, []) + assert result == obj + + +class TestGetNestedValueFromContext: + """Tests for the main get_nested_value_from_context function.""" + + @pytest.fixture + def sample_context(self): + """Create a comprehensive sample context for testing.""" + return { + "meta": { + "agent_id": "principal-agent", + "run_id": "run-abc-123", + }, + "state": { + "messages": [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ], + "flags": { + "debug": True, + "verbose": False, + }, + "initial_parameters": { + "model": "gpt-4", + "temperature": 0.7, + }, + "deliverables": { + "summary": "Task completed successfully", + }, + }, + "refs": { + "team": { + "question": "What is machine learning?", + "work_modules": {}, + }, + "run": { + "meta": {"run_id": "run-abc-123", "status": "RUNNING"}, + "config": {"model_override": None}, + }, + }, + } + + def test_state_prefix_access(self, sample_context): + """Test accessing values with 'state.' prefix.""" + assert get_nested_value_from_context(sample_context, "state.deliverables.summary") == "Task completed successfully" + + def test_implicit_state_prefix(self, sample_context): + """Test that paths without prefix default to 'state.'.""" + assert get_nested_value_from_context(sample_context, "deliverables.summary") == "Task completed successfully" + + def test_meta_prefix_access(self, sample_context): + """Test accessing values with 'meta.' prefix.""" + assert get_nested_value_from_context(sample_context, "meta.agent_id") == "principal-agent" + + def test_team_prefix_access(self, sample_context): + """Test accessing values with 'team.' prefix.""" + assert get_nested_value_from_context(sample_context, "team.question") == "What is machine learning?" + + def test_flags_shortcut(self, sample_context): + """Test 'flags.' shortcut prefix.""" + assert get_nested_value_from_context(sample_context, "flags.debug") is True + + def test_initial_params_shortcut(self, sample_context): + """Test 'initial_params.' shortcut prefix.""" + assert get_nested_value_from_context(sample_context, "initial_params.model") == "gpt-4" + + def test_run_prefix_access(self, sample_context): + """Test 'run.' prefix for run metadata.""" + assert get_nested_value_from_context(sample_context, "run.status") == "RUNNING" + + def test_list_access_by_index(self, sample_context): + """Test accessing list elements by index.""" + assert get_nested_value_from_context(sample_context, "state.messages[0].role") == "user" + assert get_nested_value_from_context(sample_context, "state.messages[-1].content") == "Hi there!" + + def test_missing_path_returns_none(self, sample_context): + """Test that missing paths return None by default.""" + result = get_nested_value_from_context(sample_context, "nonexistent.path.here") + assert result is None + + def test_missing_path_with_custom_default(self, sample_context): + """Test that missing paths return custom default when provided.""" + result = get_nested_value_from_context(sample_context, "missing.key", default="fallback") + assert result == "fallback" + + def test_empty_path_returns_none(self, sample_context): + """Test that empty path returns None.""" + assert get_nested_value_from_context(sample_context, "") is None + assert get_nested_value_from_context(sample_context, None) is None + + def test_prefix_only_returns_base_object(self, sample_context): + """Test that prefix-only paths return the base object.""" + assert get_nested_value_from_context(sample_context, "meta") == sample_context["meta"] + assert get_nested_value_from_context(sample_context, "team") == sample_context["refs"]["team"] + + def test_self_prefix_returns_entire_context(self, sample_context): + """Test '_self' prefix returns entire context.""" + result = get_nested_value_from_context(sample_context, "_self") + assert result is sample_context + + def test_deeply_nested_access(self, sample_context): + """Test accessing deeply nested values.""" + # Create deeper nesting + sample_context["state"]["deep"] = {"level1": {"level2": {"level3": {"value": 42}}}} + result = get_nested_value_from_context(sample_context, "deep.level1.level2.level3.value") + assert result == 42 + + def test_none_value_distinguished_from_missing(self, sample_context): + """Test that actual None values are returned correctly.""" + sample_context["state"]["explicit_none"] = None + result = get_nested_value_from_context(sample_context, "explicit_none") + assert result is None + + def test_non_string_path_returns_default(self): + """Test that non-string paths are handled gracefully.""" + ctx = {"state": {"foo": "bar"}} + assert get_nested_value_from_context(ctx, 123, default="fallback") == "fallback" + assert get_nested_value_from_context(ctx, ["invalid"], default="fallback") == "fallback" + + +class TestVModelAccessor: + """Tests for the VModelAccessor class.""" + + @pytest.fixture + def accessor_context(self): + """Create context and accessor for testing.""" + ctx = { + "state": { + "name": "test-name", + "items": ["a", "b", "c"], + }, + "meta": {"agent_id": "accessor-test"}, + } + return ctx, VModelAccessor(ctx) + + def test_getitem_basic_access(self, accessor_context): + """Test basic __getitem__ access.""" + ctx, accessor = accessor_context + assert accessor["state.name"] == "test-name" + + def test_getitem_with_prefix(self, accessor_context): + """Test __getitem__ with explicit prefix.""" + ctx, accessor = accessor_context + assert accessor["meta.agent_id"] == "accessor-test" + + def test_getitem_list_access(self, accessor_context): + """Test __getitem__ with list index.""" + ctx, accessor = accessor_context + assert accessor["state.items[1]"] == "b" + + def test_getitem_implicit_state_prefix(self, accessor_context): + """Test __getitem__ defaults to state prefix.""" + ctx, accessor = accessor_context + assert accessor["name"] == "test-name" + + def test_accessor_in_eval_context(self, accessor_context): + """Test that accessor works in eval() environments (its design purpose).""" + ctx, v = accessor_context + # This simulates how VModelAccessor is used in dynamic evaluation contexts + result = eval("v['state.name']", {"v": v}) + assert result == "test-name" + + +class TestDefaultPathPrefix: + """Tests for DEFAULT_PATH_PREFIX behavior.""" + + def test_default_prefix_is_state(self): + """Verify DEFAULT_PATH_PREFIX is 'state'.""" + assert DEFAULT_PATH_PREFIX == "state" + + def test_unprefixed_paths_use_state(self): + """Test that paths without known prefix resolve from state.""" + ctx = {"state": {"foo": {"bar": "baz"}}} + result = get_nested_value_from_context(ctx, "foo.bar") + assert result == "baz" + + +class TestEdgeCases: + """Tests for edge cases and error handling.""" + + def test_empty_context(self): + """Test behavior with empty context.""" + result = get_nested_value_from_context({}, "state.foo") + assert result is None + + def test_none_context(self): + """Test behavior with None context (should not raise).""" + # The function should handle None gracefully + # Based on implementation, this accesses None.get() which will fail + # but we want to verify it doesn't raise unexpectedly + try: + result = get_nested_value_from_context(None, "foo") + # If it doesn't raise, it should return None or default + assert result is None or result == SENTINEL_DEFAULT + except (AttributeError, TypeError): + # This is acceptable - None is not a valid context + pass + + def test_special_characters_in_path(self): + """Test paths with special characters (non-standard but possible).""" + ctx = {"state": {"key-with-dashes": "value1", "key_with_underscores": "value2"}} + assert get_nested_value_from_context(ctx, "key_with_underscores") == "value2" + + def test_numeric_string_keys(self): + """Test dict keys that are numeric strings.""" + ctx = {"state": {"0": "zero", "1": "one"}} + # These should be treated as dict keys, not list indices + assert get_nested_value_from_context(ctx, "0") == "zero" + + def test_boolean_values(self): + """Test accessing boolean values.""" + ctx = {"state": {"is_active": True, "is_disabled": False}} + assert get_nested_value_from_context(ctx, "is_active") is True + assert get_nested_value_from_context(ctx, "is_disabled") is False + + def test_integer_and_float_values(self): + """Test accessing numeric values.""" + ctx = {"state": {"count": 42, "ratio": 3.14}} + assert get_nested_value_from_context(ctx, "count") == 42 + assert get_nested_value_from_context(ctx, "ratio") == 3.14 + + def test_empty_string_value(self): + """Test accessing empty string values.""" + ctx = {"state": {"empty": ""}} + result = get_nested_value_from_context(ctx, "empty") + assert result == "" + + def test_list_of_dicts(self): + """Test accessing nested dicts inside lists.""" + ctx = { + "state": { + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, + ] + } + } + assert get_nested_value_from_context(ctx, "users[0].name") == "Alice" + assert get_nested_value_from_context(ctx, "users[-1].age") == 25 diff --git a/core/tests/test_dynamic_loader.py b/core/tests/test_dynamic_loader.py new file mode 100644 index 0000000..51a456b --- /dev/null +++ b/core/tests/test_dynamic_loader.py @@ -0,0 +1,149 @@ +""" +Unit tests for agent_core.framework.dynamic_loader module. + +This module tests the dynamic callable loading functionality +used to import functions/classes from string paths at runtime. + +Key functionality tested: +- get_callable_from_path: Dynamic import from 'module.function' strings +- Error handling for invalid paths, missing modules, missing attributes +""" + +import pytest +from agent_core.framework.dynamic_loader import get_callable_from_path + + +class TestGetCallableFromPath: + """Tests for get_callable_from_path function.""" + + def test_loads_builtin_function(self): + """Test loading a builtin function.""" + # json.dumps is a standard library function + func = get_callable_from_path("json.dumps") + + assert callable(func) + # Verify it's actually json.dumps by using it + result = func({"key": "value"}) + assert result == '{"key": "value"}' + + def test_loads_function_from_standard_library(self): + """Test loading from standard library modules.""" + func = get_callable_from_path("os.path.join") + + assert callable(func) + assert func("a", "b") == "a/b" or func("a", "b") == "a\\b" # OS-dependent + + def test_loads_class_from_module(self): + """Test loading a class.""" + cls = get_callable_from_path("datetime.datetime") + + assert cls is not None + # Verify it's the datetime class + instance = cls(2024, 1, 1) + assert instance.year == 2024 + + def test_loads_deeply_nested_path(self): + """Test loading from deeply nested module path.""" + func = get_callable_from_path("urllib.parse.urlparse") + + assert callable(func) + result = func("http://example.com/path") + assert result.netloc == "example.com" + + def test_raises_import_error_for_missing_module(self): + """Test raises error for non-existent module.""" + with pytest.raises(ImportError): + get_callable_from_path("nonexistent_module_xyz.some_function") + + def test_raises_attribute_error_for_missing_function(self): + """Test raises error for non-existent function in valid module.""" + with pytest.raises(AttributeError): + get_callable_from_path("json.nonexistent_function_xyz") + + def test_raises_value_error_for_no_dot(self): + """Test raises error for path without dot separator.""" + with pytest.raises(ValueError): + get_callable_from_path("nodotpath") + + def test_raises_for_empty_string(self): + """Test raises error for empty string path.""" + with pytest.raises(ValueError): + get_callable_from_path("") + + def test_loads_from_agent_core_module(self): + """Test loading from the agent_core package itself.""" + # Load a known function from agent_core + func = get_callable_from_path( + "agent_core.utils.context_helpers.get_nested_value_from_context" + ) + + assert callable(func) + + def test_loads_constant_or_module_attribute(self): + """Test loading a module-level constant/attribute.""" + # sys.version is a string attribute, not callable + version = get_callable_from_path("sys.version") + + assert isinstance(version, str) + assert "." in version # Version string contains dots + + def test_caches_import_appropriately(self): + """Test that repeated calls work (importlib handles caching).""" + func1 = get_callable_from_path("json.loads") + func2 = get_callable_from_path("json.loads") + + # Should be the same function + assert func1 is func2 + + +class TestEdgeCases: + """Edge case tests for dynamic loader.""" + + def test_path_with_multiple_dots(self): + """Test path with many nested modules.""" + func = get_callable_from_path("email.mime.text.MIMEText") + + assert func is not None + + def test_trailing_dot_raises(self): + """Test that trailing dot raises error.""" + with pytest.raises((ValueError, ImportError, AttributeError)): + get_callable_from_path("json.") + + def test_leading_dot_raises(self): + """Test that leading dot raises error (TypeError from importlib for relative import).""" + with pytest.raises((ValueError, ImportError, TypeError)): + get_callable_from_path(".json.dumps") + + def test_double_dot_raises(self): + """Test that double dot raises error.""" + with pytest.raises((ValueError, ImportError)): + get_callable_from_path("json..dumps") + + +class TestRealWorldUseCases: + """Tests simulating real-world usage patterns.""" + + def test_load_custom_node_class(self): + """Test loading a custom node class pattern.""" + # This simulates how the agent system loads custom nodes + # Using a standard library class as proxy + cls = get_callable_from_path("collections.OrderedDict") + + instance = cls() + instance["key"] = "value" + assert list(instance.keys()) == ["key"] + + def test_load_strategy_function(self): + """Test loading a strategy/handler function pattern.""" + # Simulates loading event handlers dynamically + func = get_callable_from_path("operator.add") + + assert func(2, 3) == 5 + + def test_load_validation_callable(self): + """Test loading a validation function.""" + func = get_callable_from_path("re.compile") + + pattern = func(r"\d+") + assert pattern.match("123") is not None diff --git a/core/tests/test_embedding_utils.py b/core/tests/test_embedding_utils.py new file mode 100644 index 0000000..1969066 --- /dev/null +++ b/core/tests/test_embedding_utils.py @@ -0,0 +1,573 @@ +""" +Unit tests for agent_core/rag/embedding_utils.py + +Tests embedding providers, quantization functions, and utility functions. +""" + +import pytest +import numpy as np +from unittest.mock import patch, MagicMock +import os + +from agent_core.rag.embedding_utils import ( + fast_8bit_uniform_scalar_quantize, + fast_4bit_uniform_scalar_quantize, + l2_normalize_numpy_pytorch_like, + EmbeddingProvider, + LocalModelProvider, + JinaAPIProvider, + get_embedding_provider, + _PROVIDER_CACHE, + _DEFAULT_MODEL_PATH, +) + + +class TestFast8BitUniformScalarQuantize: + """Tests for 8-bit quantization function.""" + + def test_basic_quantization(self): + """Test basic 8-bit quantization on simple input.""" + emb = np.array([[0.0, 0.5, -0.5]], dtype=np.float32) + limit = 1.0 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.dtype == np.uint8 + assert result.shape == emb.shape + # 0.0 should map to middle (127 or 128) + assert 125 <= result[0, 0] <= 130 # Approximately middle + + def test_quantization_range(self): + """Test that output stays in valid uint8 range.""" + # Values well within limits + emb = np.array([[0.9, -0.9, 0.1, -0.1]], dtype=np.float32) + limit = 1.0 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.min() >= 0 + assert result.max() <= 255 + + def test_values_at_limits(self): + """Test values at the limit boundaries.""" + limit = 1.0 + emb = np.array([[-1.0, 1.0]], dtype=np.float32) + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + # -limit should map to 0, +limit should map to 255 + assert result[0, 0] == 0 # -1.0 at limit + assert result[0, 1] == 255 # +1.0 at limit + + def test_values_beyond_limits_clipped(self): + """Test that values beyond limits are clipped.""" + limit = 0.5 + emb = np.array([[-1.0, 1.0]], dtype=np.float32) # Beyond limit + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + # Should be clipped to 0 and 255 + assert result[0, 0] == 0 + assert result[0, 1] == 255 + + def test_batch_processing(self): + """Test quantization handles batches correctly.""" + emb = np.random.randn(100, 128).astype(np.float32) * 0.5 + limit = 1.0 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (100, 128) + assert result.dtype == np.uint8 + + +class TestFast4BitUniformScalarQuantize: + """Tests for 4-bit quantization function.""" + + def test_basic_quantization_packs_pairs(self): + """Test that 4-bit quantization packs pairs of values.""" + # Must have even number of columns + emb = np.array([[0.0, 0.5, -0.5, 0.25]], dtype=np.float32) + limit = 1.0 + + result = fast_4bit_uniform_scalar_quantize(emb, limit) + + assert result.dtype == np.uint8 + # Output should have half the columns (pairs packed) + assert result.shape == (1, 2) + + def test_output_shape(self): + """Test output shape is half the input columns.""" + emb = np.random.randn(10, 256).astype(np.float32) * 0.5 + limit = 1.0 + + result = fast_4bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (10, 128) + + def test_requires_even_columns(self): + """Test that odd column count raises assertion error.""" + emb = np.array([[0.0, 0.5, -0.5]], dtype=np.float32) # 3 columns (odd) + limit = 1.0 + + with pytest.raises(AssertionError): + fast_4bit_uniform_scalar_quantize(emb, limit) + + def test_quantization_range(self): + """Test that output stays in valid uint8 range.""" + emb = np.random.randn(50, 128).astype(np.float32) * 0.5 + limit = 1.0 + + result = fast_4bit_uniform_scalar_quantize(emb, limit) + + assert result.min() >= 0 + assert result.max() <= 255 + + +class TestL2NormalizeNumpyPytorchLike: + """Tests for L2 normalization function.""" + + def test_basic_normalization(self): + """Test that vectors are normalized to unit length.""" + arr = np.array([[3.0, 4.0]], dtype=np.float32) # Length 5 + + result = l2_normalize_numpy_pytorch_like(arr, axis=1) + + # Check unit length + norm = np.linalg.norm(result, axis=1) + np.testing.assert_almost_equal(norm, [1.0]) + # Check values: 3/5=0.6, 4/5=0.8 + np.testing.assert_almost_equal(result[0], [0.6, 0.8]) + + def test_batch_normalization(self): + """Test normalization of multiple vectors.""" + arr = np.array([ + [3.0, 4.0], + [1.0, 0.0], + [0.0, 2.0], + ], dtype=np.float32) + + result = l2_normalize_numpy_pytorch_like(arr, axis=1) + + # All vectors should have unit length + norms = np.linalg.norm(result, axis=1) + np.testing.assert_almost_equal(norms, [1.0, 1.0, 1.0]) + + def test_handles_zero_vector_with_epsilon(self): + """Test that zero vectors don't cause division by zero.""" + arr = np.array([[0.0, 0.0, 0.0]], dtype=np.float32) + epsilon = 1e-12 + + result = l2_normalize_numpy_pytorch_like(arr, axis=1, epsilon=epsilon) + + # Should not raise, output should be valid + assert result.shape == arr.shape + assert np.isfinite(result).all() + + def test_raises_for_non_ndarray(self): + """Test that non-ndarray input raises TypeError.""" + with pytest.raises(TypeError, match="must be a NumPy ndarray"): + l2_normalize_numpy_pytorch_like([1, 2, 3], axis=1) + + def test_raises_for_none_axis(self): + """Test that None axis raises ValueError.""" + arr = np.array([[1.0, 2.0]], dtype=np.float32) + + with pytest.raises(ValueError, match="axis.*must be specified"): + l2_normalize_numpy_pytorch_like(arr, axis=None) + + def test_handles_integer_input(self): + """Test that integer arrays are converted to float.""" + arr = np.array([[3, 4]], dtype=np.int32) + + result = l2_normalize_numpy_pytorch_like(arr, axis=1) + + assert result.dtype in [np.float32, np.float64] + np.testing.assert_almost_equal(result[0], [0.6, 0.8]) + + def test_high_dimensional_vectors(self): + """Test normalization of high-dimensional vectors.""" + arr = np.random.randn(10, 768).astype(np.float32) + + result = l2_normalize_numpy_pytorch_like(arr, axis=1) + + norms = np.linalg.norm(result, axis=1) + np.testing.assert_almost_equal(norms, np.ones(10), decimal=5) + + +class TestLocalModelProvider: + """Tests for LocalModelProvider class.""" + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_initialization_with_model_id(self, mock_text_embedding): + """Test provider initialization with model ID.""" + provider = LocalModelProvider("test-model") + + mock_text_embedding.assert_called_once_with(model_name_or_path="test-model") + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_initialization_with_config(self, mock_text_embedding): + """Test provider initialization with config.""" + config = {"device": "cpu", "batch_size": 32} + + provider = LocalModelProvider("test-model", config) + + mock_text_embedding.assert_called_once_with( + model_name_or_path="test-model", + model_config={"device": "cpu", "batch_size": 32} + ) + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_filters_api_specific_keys_from_config(self, mock_text_embedding): + """Test that API-specific keys are filtered from config.""" + config = { + "device": "cpu", + "api_model_name": "should-be-removed", + "api_key_env_var": "should-be-removed" + } + + provider = LocalModelProvider("test-model", config) + + # Only non-API keys should be passed + call_args = mock_text_embedding.call_args + if call_args.kwargs.get("model_config"): + assert "api_model_name" not in call_args.kwargs["model_config"] + assert "api_key_env_var" not in call_args.kwargs["model_config"] + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_generate_embedding_empty_texts(self, mock_text_embedding): + """Test generate_embedding with empty list.""" + provider = LocalModelProvider("test-model") + + result = provider.generate_embedding([]) + + assert isinstance(result, np.ndarray) + assert result.size == 0 + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_generate_embedding_calls_encode(self, mock_text_embedding): + """Test that generate_embedding calls model.encode.""" + mock_model = MagicMock() + mock_model.model_name_or_path = "test-model" + # Return mock embeddings + mock_model.encode.return_value = np.random.randn(2, 256).astype(np.float32) + mock_text_embedding.return_value = mock_model + + provider = LocalModelProvider("test-model") + result = provider.generate_embedding(["hello", "world"]) + + mock_model.encode.assert_called_once() + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_snowflake_model_prepends_query_prefix(self, mock_text_embedding): + """Test that Snowflake models prepend 'query: ' for query task.""" + mock_model = MagicMock() + mock_model.model_name_or_path = "Snowflake/snowflake-arctic-embed-m-v2.0" + mock_model.encode.return_value = np.random.randn(1, 256).astype(np.float32) + mock_text_embedding.return_value = mock_model + + provider = LocalModelProvider("Snowflake/snowflake-arctic-embed-m-v2.0") + provider.generate_embedding(["test text"], task_type="query") + + # Check that the text was prefixed + call_args = mock_model.encode.call_args + assert "query: test text" in call_args[0][0] + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_mrl_truncates_dimensions(self, mock_text_embedding): + """Test that MRL parameter truncates embedding dimensions.""" + mock_model = MagicMock() + mock_model.model_name_or_path = "test-model" + # Return 768-dim embeddings + mock_model.encode.return_value = np.random.randn(2, 768).astype(np.float32) + mock_text_embedding.return_value = mock_model + + provider = LocalModelProvider("test-model") + result = provider.generate_embedding(["hello", "world"], mrl=128) + + # Should be truncated to 128 dims + assert result.shape == (2, 128) + + @patch("agent_core.rag.embedding_utils.TextEmbedding") + def test_mrl_none_returns_full_dimensions(self, mock_text_embedding): + """Test that MRL=None returns full dimensions.""" + mock_model = MagicMock() + mock_model.model_name_or_path = "test-model" + mock_model.encode.return_value = np.random.randn(2, 768).astype(np.float32) + mock_text_embedding.return_value = mock_model + + provider = LocalModelProvider("test-model") + result = provider.generate_embedding(["hello", "world"], mrl=None) + + assert result.shape == (2, 768) + + +class TestJinaAPIProvider: + """Tests for JinaAPIProvider class.""" + + def test_initialization_defaults(self): + """Test provider uses defaults when no config.""" + provider = JinaAPIProvider() + + assert provider.api_model_name == "jina-embeddings-v3" + assert provider.api_key_env_var == "JINA_KEY" + assert provider.api_url == 'https://api.jina.ai/v1/embeddings' + + def test_initialization_with_config(self): + """Test provider uses config values.""" + config = { + "api_model_name": "custom-model", + "api_key_env_var": "CUSTOM_KEY" + } + + provider = JinaAPIProvider(config) + + assert provider.api_model_name == "custom-model" + assert provider.api_key_env_var == "CUSTOM_KEY" + + def test_generate_embedding_empty_texts(self): + """Test generate_embedding with empty list.""" + provider = JinaAPIProvider() + + result = provider.generate_embedding([]) + + assert isinstance(result, np.ndarray) + assert result.size == 0 + + def test_generate_embedding_raises_without_api_key(self): + """Test generate_embedding raises when API key not set.""" + provider = JinaAPIProvider({"api_key_env_var": "NONEXISTENT_KEY"}) + + with patch.dict(os.environ, {}, clear=False): + os.environ.pop("NONEXISTENT_KEY", None) + + with pytest.raises(ValueError, match="API key not found"): + provider.generate_embedding(["test"]) + + @patch("agent_core.rag.embedding_utils.requests.post") + def test_generate_embedding_makes_api_call(self, mock_post): + """Test that generate_embedding makes correct API call.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": [ + {"embedding": [0.1, 0.2, 0.3] * 50} # 150 dims + ] + } + mock_post.return_value = mock_response + + provider = JinaAPIProvider() + + with patch.dict(os.environ, {"JINA_KEY": "test-key"}): + result = provider.generate_embedding(["hello"], mrl=128) + + mock_post.assert_called_once() + call_args = mock_post.call_args + + # Check URL + assert call_args[0][0] == 'https://api.jina.ai/v1/embeddings' + # Check headers + assert "Authorization" in call_args[1]["headers"] + assert "Bearer test-key" in call_args[1]["headers"]["Authorization"] + # Check payload + assert call_args[1]["json"]["input"] == ["hello"] + + @patch("agent_core.rag.embedding_utils.requests.post") + def test_generate_embedding_includes_task(self, mock_post): + """Test that task_type is included in API request.""" + mock_response = MagicMock() + mock_response.json.return_value = { + "data": [{"embedding": [0.1] * 256}] + } + mock_post.return_value = mock_response + + provider = JinaAPIProvider() + + with patch.dict(os.environ, {"JINA_KEY": "test-key"}): + provider.generate_embedding(["hello"], task_type="retrieval.query") + + call_args = mock_post.call_args + assert call_args[1]["json"]["task"] == "retrieval.query" + + @patch("agent_core.rag.embedding_utils.requests.post") + def test_generate_embedding_handles_request_error(self, mock_post): + """Test graceful handling of request errors.""" + import requests + mock_post.side_effect = requests.exceptions.RequestException("Connection failed") + + provider = JinaAPIProvider() + + with patch.dict(os.environ, {"JINA_KEY": "test-key"}): + result = provider.generate_embedding(["hello"]) + + # Should return empty array on error + assert isinstance(result, np.ndarray) + assert result.size == 0 + + @patch("agent_core.rag.embedding_utils.requests.post") + def test_generate_embedding_handles_malformed_response(self, mock_post): + """Test handling of malformed API response.""" + mock_response = MagicMock() + mock_response.json.return_value = {"error": "invalid"} # Missing 'data' + mock_post.return_value = mock_response + + provider = JinaAPIProvider() + + with patch.dict(os.environ, {"JINA_KEY": "test-key"}): + result = provider.generate_embedding(["hello"]) + + # Should return empty array on malformed response + assert result.size == 0 + + +class TestGetEmbeddingProvider: + """Tests for the provider factory function.""" + + def setup_method(self): + """Clear provider cache before each test.""" + _PROVIDER_CACHE.clear() + + @patch("agent_core.rag.embedding_utils.LocalModelProvider") + def test_returns_local_provider_for_model_path(self, mock_local_provider): + """Test that model paths return LocalModelProvider.""" + mock_instance = MagicMock() + mock_local_provider.return_value = mock_instance + + provider = get_embedding_provider("Snowflake/snowflake-arctic-embed-m-v2.0") + + mock_local_provider.assert_called_once_with( + "Snowflake/snowflake-arctic-embed-m-v2.0", None + ) + + @patch("agent_core.rag.embedding_utils.JinaAPIProvider") + def test_returns_jina_provider_for_jina_api(self, mock_jina_provider): + """Test that 'jina-api' returns JinaAPIProvider.""" + mock_instance = MagicMock() + mock_jina_provider.return_value = mock_instance + + provider = get_embedding_provider("jina-api") + + mock_jina_provider.assert_called_once_with(None) + + @patch("agent_core.rag.embedding_utils.LocalModelProvider") + def test_uses_default_model_when_none(self, mock_local_provider): + """Test that None model_id uses default.""" + mock_instance = MagicMock() + mock_local_provider.return_value = mock_instance + + provider = get_embedding_provider(None) + + mock_local_provider.assert_called_once_with(_DEFAULT_MODEL_PATH, None) + + @patch("agent_core.rag.embedding_utils.LocalModelProvider") + def test_caches_providers(self, mock_local_provider): + """Test that providers are cached.""" + mock_instance = MagicMock() + mock_local_provider.return_value = mock_instance + + provider1 = get_embedding_provider("test-model") + provider2 = get_embedding_provider("test-model") + + # Should only create once + assert mock_local_provider.call_count == 1 + assert provider1 is provider2 + + @patch("agent_core.rag.embedding_utils.LocalModelProvider") + def test_cache_key_includes_config(self, mock_local_provider): + """Test that different configs create different cache entries.""" + mock_local_provider.side_effect = [MagicMock(), MagicMock()] + + provider1 = get_embedding_provider("test-model", {"batch_size": 16}) + provider2 = get_embedding_provider("test-model", {"batch_size": 32}) + + # Should create two different providers + assert mock_local_provider.call_count == 2 + assert provider1 is not provider2 + + @patch("agent_core.rag.embedding_utils.LocalModelProvider") + def test_passes_config_to_provider(self, mock_local_provider): + """Test that model_config is passed to provider.""" + mock_instance = MagicMock() + mock_local_provider.return_value = mock_instance + config = {"device": "cuda"} + + provider = get_embedding_provider("test-model", config) + + mock_local_provider.assert_called_once_with("test-model", config) + + +class TestEmbeddingProviderAbstract: + """Tests for EmbeddingProvider abstract base class.""" + + def test_cannot_instantiate_directly(self): + """Test that EmbeddingProvider cannot be instantiated directly.""" + with pytest.raises(TypeError): + EmbeddingProvider() + + def test_subclass_must_implement_generate_embedding(self): + """Test that subclasses must implement generate_embedding.""" + class IncompleteProvider(EmbeddingProvider): + pass + + with pytest.raises(TypeError): + IncompleteProvider() + + def test_subclass_with_implementation_works(self): + """Test that complete subclass can be instantiated.""" + class CompleteProvider(EmbeddingProvider): + def generate_embedding(self, texts, task_type="", mrl=128): + return np.array([]) + + provider = CompleteProvider() + assert isinstance(provider, EmbeddingProvider) + + +class TestQuantizationEdgeCases: + """Edge case tests for quantization functions.""" + + def test_8bit_single_value(self): + """Test 8-bit quantization with single value.""" + emb = np.array([[0.0]], dtype=np.float32) + limit = 1.0 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (1, 1) + + def test_8bit_very_small_limit(self): + """Test 8-bit quantization with very small limit.""" + emb = np.array([[0.0, 0.001, -0.001]], dtype=np.float32) + limit = 0.01 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.dtype == np.uint8 + + def test_4bit_minimum_valid_size(self): + """Test 4-bit quantization with minimum valid size (2 columns).""" + emb = np.array([[0.0, 0.5]], dtype=np.float32) + limit = 1.0 + + result = fast_4bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (1, 1) + + def test_large_batch_8bit(self): + """Test 8-bit quantization with large batch.""" + emb = np.random.randn(1000, 512).astype(np.float32) * 0.3 + limit = 1.0 + + result = fast_8bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (1000, 512) + assert result.dtype == np.uint8 + + def test_large_batch_4bit(self): + """Test 4-bit quantization with large batch.""" + emb = np.random.randn(1000, 512).astype(np.float32) * 0.3 + limit = 1.0 + + result = fast_4bit_uniform_scalar_quantize(emb, limit) + + assert result.shape == (1000, 256) + assert result.dtype == np.uint8 diff --git a/core/tests/test_event_strategies.py b/core/tests/test_event_strategies.py new file mode 100644 index 0000000..fa9c4c5 --- /dev/null +++ b/core/tests/test_event_strategies.py @@ -0,0 +1,698 @@ +""" +Tier 3 Unit Tests for agent_core/events/event_strategies.py and agent_core/events/ingestors.py + +Tests the event handling system: +- EventHandlingStrategy class +- EVENT_STRATEGY_REGISTRY configuration +- Ingestor functions for various event types + +Test Categories: +1. EventHandlingStrategy: Structure and configuration +2. Ingestor Registry: Registration and retrieval +3. Core Ingestors: templated_content, generic_message, tool_result +4. Specialized Ingestors: markdown_formatter, work_modules, json_history +5. Helper Functions: _apply_simple_template_interpolation, _recursive_markdown_formatter +""" + +import pytest +from unittest.mock import patch, MagicMock +import json + +from agent_core.events.event_strategies import ( + EventHandlingStrategy, + EVENT_STRATEGY_REGISTRY +) +from agent_core.events.ingestors import ( + INGESTOR_REGISTRY, + register_ingestor, + _apply_simple_template_interpolation, + templated_content_ingestor, + generic_message_ingestor, + tool_result_ingestor, + markdown_formatter_ingestor, + work_modules_ingestor, + principal_history_summary_ingestor, + json_history_ingestor, + tagged_content_ingestor, + observer_failure_ingestor, + user_prompt_ingestor, + protocol_aware_ingestor, + _recursive_markdown_formatter +) + + +class TestEventHandlingStrategy: + """Tests for the EventHandlingStrategy class.""" + + def test_init_stores_all_attributes(self): + """Test that all attributes are stored correctly.""" + def mock_ingestor(p, params, ctx): + return "result" + + strategy = EventHandlingStrategy( + ingestor_func=mock_ingestor, + default_injection_mode="append_as_new_message", + default_params={"role": "user", "persistent": True} + ) + + assert strategy.ingestor is mock_ingestor + assert strategy.default_injection_mode == "append_as_new_message" + assert strategy.default_params["role"] == "user" + assert strategy.default_params["persistent"] is True + + def test_strategy_callable(self): + """Test that the ingestor function is callable through strategy.""" + def echo_ingestor(payload, params, ctx): + return f"Received: {payload}" + + strategy = EventHandlingStrategy( + ingestor_func=echo_ingestor, + default_injection_mode="prepend", + default_params={} + ) + + result = strategy.ingestor("test payload", {}, {}) + assert result == "Received: test payload" + + +class TestEventStrategyRegistry: + """Tests for the EVENT_STRATEGY_REGISTRY configuration.""" + + def test_registry_has_expected_event_types(self): + """Test that all expected event types are registered.""" + expected_types = [ + "TOOL_RESULT", + "AGENT_STARTUP_BRIEFING", + "SELF_REFLECTION_PROMPT", + "INTERNAL_DIRECTIVE", + "PARTNER_DIRECTIVE", + "PRINCIPAL_COMPLETED", + "WORK_MODULES_STATUS_UPDATE", + "PRINCIPAL_ACTIVITY_UPDATE", + "FIM_INSTRUCTION", + "JSON_HISTORY_FOR_LLM", + "TOOL_INPUTS_BRIEFING", + "ORIGINAL_QUESTION", + "OBSERVER_FAILURE", + "USER_PROMPT" + ] + + for event_type in expected_types: + assert event_type in EVENT_STRATEGY_REGISTRY, f"Missing: {event_type}" + + def test_tool_result_strategy_config(self): + """Test TOOL_RESULT strategy configuration.""" + strategy = EVENT_STRATEGY_REGISTRY["TOOL_RESULT"] + + assert strategy.default_injection_mode == "append_as_new_message" + assert strategy.default_params["role"] == "tool" + assert strategy.default_params["is_persistent_in_memory"] is True + assert strategy.ingestor is tool_result_ingestor + + def test_agent_startup_briefing_strategy_config(self): + """Test AGENT_STARTUP_BRIEFING strategy configuration.""" + strategy = EVENT_STRATEGY_REGISTRY["AGENT_STARTUP_BRIEFING"] + + assert strategy.default_injection_mode == "append_as_new_message" + assert strategy.default_params["role"] == "user" + assert strategy.ingestor is protocol_aware_ingestor + + def test_partner_directive_has_formatting_params(self): + """Test PARTNER_DIRECTIVE has special formatting parameters.""" + strategy = EVENT_STRATEGY_REGISTRY["PARTNER_DIRECTIVE"] + + assert "title" in strategy.default_params + assert "key_renames" in strategy.default_params + assert strategy.default_params["key_renames"]["content"] == "Instruction" + + def test_observer_failure_is_transient(self): + """Test OBSERVER_FAILURE is configured as non-persistent.""" + strategy = EVENT_STRATEGY_REGISTRY["OBSERVER_FAILURE"] + + assert strategy.default_params["is_persistent_in_memory"] is False + assert strategy.default_params["role"] == "system" + + +class TestRegisterIngestor: + """Tests for the @register_ingestor decorator.""" + + def test_decorator_registers_function(self): + """Test that the decorator adds function to registry.""" + @register_ingestor("test_custom_ingestor") + def custom_ingestor(payload, params, context): + return "custom" + + assert "test_custom_ingestor" in INGESTOR_REGISTRY + assert INGESTOR_REGISTRY["test_custom_ingestor"] is custom_ingestor + + def test_decorator_allows_override_with_warning(self): + """Test that overriding an ingestor logs a warning.""" + @register_ingestor("override_test") + def first_version(p, params, c): + return "first" + + with patch('agent_core.events.ingestors.logger.warning') as mock_warn: + @register_ingestor("override_test") + def second_version(p, params, c): + return "second" + + mock_warn.assert_called() + + assert INGESTOR_REGISTRY["override_test"]({}, {}, {}) == "second" + + +class TestApplySimpleTemplateInterpolation: + """Tests for the _apply_simple_template_interpolation helper.""" + + def test_no_template_markers(self): + """Test text without template markers is unchanged.""" + result = _apply_simple_template_interpolation("plain text", {}) + assert result == "plain text" + + def test_single_variable_replacement(self): + """Test single variable replacement.""" + context = {"state": {"user_name": "Alice"}} + result = _apply_simple_template_interpolation( + "Hello, {{ state.user_name }}!", + context + ) + assert result == "Hello, Alice!" + + def test_multiple_variable_replacement(self): + """Test multiple variables in same string.""" + context = { + "state": {"name": "Bob"}, + "meta": {"count": 5} + } + result = _apply_simple_template_interpolation( + "{{ state.name }} has {{ meta.count }} items", + context + ) + assert result == "Bob has 5 items" + + def test_missing_variable_keeps_template(self): + """Test that missing variables keep original template marker.""" + context = {"state": {}} + result = _apply_simple_template_interpolation( + "Value: {{ state.missing }}", + context + ) + assert result == "Value: {{ state.missing }}" + + def test_non_string_input(self): + """Test that non-string input is returned unchanged.""" + result = _apply_simple_template_interpolation(123, {}) + assert result == 123 + + def test_whitespace_in_template(self): + """Test that whitespace in templates is handled.""" + # The function uses get_nested_value_from_context which expects dotted paths + # Keys must be accessible via the context path resolution + context = {"state": {"key": "value"}} + result = _apply_simple_template_interpolation( + "{{ state.key }}", + context + ) + assert result == "value" + + +class TestTemplatedContentIngestor: + """Tests for the templated_content_ingestor function.""" + + def test_invalid_payload_returns_error(self): + """Test that invalid payload returns error message.""" + result = templated_content_ingestor("not a dict", {}, {}) + assert "[Error:" in result + + def test_missing_content_key_returns_error(self): + """Test that missing content_key returns error.""" + result = templated_content_ingestor({"other": "value"}, {}, {}) + assert "[Error:" in result + + def test_template_not_found_returns_error(self): + """Test that missing template returns error.""" + context = { + "loaded_profile": { + "name": "TestProfile", + "text_definitions": {} + } + } + result = templated_content_ingestor( + {"content_key": "nonexistent"}, + {}, + context + ) + assert "[Error:" in result + assert "nonexistent" in result + + def test_template_found_and_interpolated(self): + """Test that template is found and interpolated.""" + context = { + "loaded_profile": { + "name": "TestProfile", + "text_definitions": { + "greeting": "Hello, {{ state.user }}!" + } + }, + "state": {"user": "World"} + } + result = templated_content_ingestor( + {"content_key": "greeting"}, + {}, + context + ) + assert result == "Hello, World!" + + def test_wrapper_tags_applied(self): + """Test that wrapper tags are applied.""" + context = { + "loaded_profile": { + "text_definitions": {"msg": "content"} + } + } + result = templated_content_ingestor( + {"content_key": "msg"}, + {"wrapper_tags": ["", ""]}, + context + ) + assert result == "content" + + +class TestGenericMessageIngestor: + """Tests for the generic_message_ingestor function.""" + + def test_default_template(self): + """Test with default template.""" + result = generic_message_ingestor("test payload", {}, {}) + assert result == "test payload" + + def test_custom_template_with_payload(self): + """Test custom template with payload placeholder.""" + result = generic_message_ingestor( + "my value", + {"content_template": "Result: {{ payload }}"}, + {} + ) + assert result == "Result: my value" + + def test_dict_payload_with_key_replacement(self): + """Test dict payload with specific key replacements.""" + result = generic_message_ingestor( + {"name": "Alice", "count": 5}, + {"content_template": "{{ payload.name }} has {{ payload.count }} items"}, + {} + ) + assert result == "Alice has 5 items" + + +class TestToolResultIngestor: + """Tests for the tool_result_ingestor function.""" + + def test_non_dict_returns_string(self): + """Test that non-dict payload is stringified.""" + result = tool_result_ingestor("simple result", {}, {}) + assert result == "simple result" + + def test_dehydrated_token_returned_directly(self): + """Test that dehydrated tokens are returned unchanged.""" + payload = {"content": "<#CGKB-abc123-def456>", "tool_name": "test"} + result = tool_result_ingestor(payload, {}, {}) + assert result == "<#CGKB-abc123-def456>" + + def test_error_wrapped_with_tags(self): + """Test that errors are wrapped with error tags.""" + payload = { + "tool_name": "failing_tool", + "is_error": True, + "content": {"reason": "Connection failed"} + } + result = tool_result_ingestor(payload, {}, {}) + + assert "" in result + assert "" in result + assert "failing_tool" in result + + def test_main_content_for_llm_prioritized(self): + """Test that main_content_for_llm is used when present.""" + payload = { + "tool_name": "smart_tool", + "content": { + "main_content_for_llm": {"summary": "Important info"}, + "extra_data": "ignored" + } + } + result = tool_result_ingestor(payload, {}, {}) + + assert "Important info" in result + assert "extra_data" not in result + + def test_raw_json_escape_hatch(self): + """Test that _raw_json returns JSON directly.""" + raw_data = {"key": "value", "nested": {"inner": 123}} + payload = { + "tool_name": "json_tool", + "content": {"_raw_json": raw_data} + } + result = tool_result_ingestor(payload, {}, {}) + + assert json.loads(result) == raw_data + + +class TestMarkdownFormatterIngestor: + """Tests for the markdown_formatter_ingestor function.""" + + def test_non_dict_stringified(self): + """Test that non-dict payloads are stringified.""" + result = markdown_formatter_ingestor(["a", "list"], {}, {}) + assert result == "['a', 'list']" + + def test_default_title(self): + """Test default title is applied.""" + result = markdown_formatter_ingestor({"key": "value"}, {}, {}) + assert "### Contextual Information" in result + + def test_custom_title(self): + """Test custom title is applied.""" + result = markdown_formatter_ingestor( + {"key": "value"}, + {"title": "### Custom Title"}, + {} + ) + assert "### Custom Title" in result + + def test_key_renames(self): + """Test that key renames are applied.""" + result = markdown_formatter_ingestor( + {"old_key": "value"}, + {"key_renames": {"old_key": "New Key Name"}}, + {} + ) + assert "**New Key Name**" in result + + def test_exclude_keys(self): + """Test that excluded keys are omitted.""" + result = markdown_formatter_ingestor( + {"visible": "yes", "hidden": "no"}, + {"exclude_keys": ["hidden"]}, + {} + ) + # The key is title-cased in output, so check for "Visible" not "visible" + assert "Visible" in result + assert "hidden" not in result.lower() # Check that hidden key is excluded + + +class TestWorkModulesIngestor: + """Tests for the work_modules_ingestor function.""" + + def test_non_dict_returns_error_message(self): + """Test that non-dict returns error message.""" + result = work_modules_ingestor("not a dict", {}, {}) + assert "not in the expected format" in result + + def test_empty_dict_shows_no_modules(self): + """Test empty dict shows no modules message.""" + result = work_modules_ingestor({}, {}, {}) + assert "No work modules" in result + + def test_excludes_large_fields(self): + """Test that large fields like context_archive are excluded.""" + payload = { + "mod_1": { + "status": "completed", + "context_archive": ["msg1", "msg2"] * 1000, # Large + "deliverables": {"summary": "Result"} + } + } + result = work_modules_ingestor(payload, {}, {}) + + assert "status" in result.lower() + assert "context_archive" not in result + + def test_summarizes_deliverables(self): + """Test that deliverables are summarized.""" + payload = { + "mod_1": { + "status": "done", + "deliverables": {"a": 1, "b": 2, "c": 3} + } + } + result = work_modules_ingestor(payload, {}, {}) + + assert "(3 items)" in result + + +class TestPrincipalHistorySummaryIngestor: + """Tests for the principal_history_summary_ingestor function.""" + + def test_non_list_returns_default(self): + """Test that non-list returns default message.""" + result = principal_history_summary_ingestor("not a list", {}, {}) + assert "no recorded activity" in result + + def test_empty_list_returns_default(self): + """Test that empty list returns default message.""" + result = principal_history_summary_ingestor([], {}, {}) + assert "no recorded activity" in result + + def test_formats_messages(self): + """Test that messages are formatted.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there"} + ] + result = principal_history_summary_ingestor(messages, {}, {}) + + assert "[USER]" in result + assert "[ASSISTANT]" in result + assert "Hello" in result + + def test_truncates_long_content(self): + """Test that long content is truncated.""" + messages = [ + {"role": "user", "content": "x" * 500} + ] + result = principal_history_summary_ingestor(messages, {}, {}) + + assert "..." in result + assert len(result) < 600 # Definitely truncated + + def test_includes_tool_calls(self): + """Test that tool calls are included.""" + messages = [ + { + "role": "assistant", + "content": "Using tool", + "tool_calls": [ + {"function": {"name": "search_web", "arguments": '{"q": "test"}'}} + ] + } + ] + result = principal_history_summary_ingestor(messages, {}, {}) + + assert "search_web" in result + + def test_respects_max_messages(self): + """Test that max_messages limit is respected.""" + messages = [{"role": "user", "content": f"msg{i}"} for i in range(20)] + result = principal_history_summary_ingestor(messages, {"max_messages": 5}, {}) + + assert "omitting" in result + + +class TestJsonHistoryIngestor: + """Tests for the json_history_ingestor function.""" + + def test_non_list_returns_error(self): + """Test that non-list returns error message.""" + result = json_history_ingestor({"not": "list"}, {}, {}) + assert "[Error:" in result + + def test_valid_list_serialized(self): + """Test that valid list is serialized to JSON.""" + history = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi"} + ] + result = json_history_ingestor(history, {}, {}) + + assert "" in result + assert "" in result + parsed = json.loads(result.split("")[1].split("")[0]) + assert parsed == history + + +class TestTaggedContentIngestor: + """Tests for the tagged_content_ingestor function.""" + + def test_with_wrapper_tags(self): + """Test content wrapped with tags.""" + result = tagged_content_ingestor( + "my content", + {"wrapper_tags": ["", ""]}, + {} + ) + assert result == "my content" + + def test_without_wrapper_tags(self): + """Test content without tags returns stringified.""" + result = tagged_content_ingestor("content", {}, {}) + assert result == "content" + + def test_invalid_wrapper_tags_format(self): + """Test invalid wrapper tags format returns plain content.""" + result = tagged_content_ingestor( + "content", + {"wrapper_tags": "not a list"}, + {} + ) + assert result == "content" + + +class TestObserverFailureIngestor: + """Tests for the observer_failure_ingestor function.""" + + def test_non_dict_returns_error(self): + """Test that non-dict returns error message.""" + result = observer_failure_ingestor("not dict", {}, {}) + assert "[Error:" in result + + def test_formats_failure_details(self): + """Test that failure details are formatted.""" + payload = { + "failed_observer_id": "context_observer_1", + "error_message": "Database connection failed" + } + result = observer_failure_ingestor(payload, {}, {}) + + assert "= 100 + + def test_no_truncation_with_budget(self): + """Even with a small budget, included findings should be complete.""" + # Create a finding that's larger than the budget + long_content = "Important finding with critical details. " * 100 # ~4000 chars + + messages = [ + {"role": "assistant", "content": long_content} + ] + + # Set a budget smaller than the content + result = _extract_deliverables_from_messages( + messages, + summarization_budget_chars=1000 + ) + summary = result.get("primary_summary", "") + + # First finding should ALWAYS be included completely + assert summary.count("Important finding") >= 50 + + def test_budget_selects_fewer_findings_not_truncate(self): + """Budget should select fewer findings, not truncate them.""" + # Create 5 distinct findings + messages = [ + {"role": "assistant", "content": f"Finding {i}: " + "x" * 200} + for i in range(1, 6) + ] + + # With no budget, should include all 5 + result_no_budget = _extract_deliverables_from_messages(messages) + summary_no_budget = result_no_budget.get("primary_summary", "") + + # With tight budget, should include fewer + result_with_budget = _extract_deliverables_from_messages( + messages, + summarization_budget_chars=500 + ) + summary_with_budget = result_with_budget.get("primary_summary", "") + + # Count how many "Finding X:" appear in each + no_budget_count = sum(1 for i in range(1, 6) if f"Finding {i}:" in summary_no_budget) + with_budget_count = sum(1 for i in range(1, 6) if f"Finding {i}:" in summary_with_budget) + + # Budget version should have fewer findings + assert with_budget_count < no_budget_count + # But at least one finding should be present + assert with_budget_count >= 1 + + # ========================================================================= + # Content Cleaning + # ========================================================================= + + def test_removes_thinking_tags(self): + """ tags should be stripped from content.""" + messages = [ + { + "role": "assistant", + "content": "Internal reasoning here\n\nThe actual analysis shows important results that should be preserved in the output." + } + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + assert "Internal reasoning" not in summary + assert "actual analysis" in summary + + def test_removes_internal_tags(self): + """ tags should be stripped from content.""" + messages = [ + { + "role": "assistant", + "content": "System note\n\nThe visible content that users should see in the final output summary." + } + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + assert "System note" not in summary + assert "visible content" in summary + + def test_removes_internal_system_directive_tags(self): + """ tags should be stripped.""" + messages = [ + { + "role": "assistant", + "content": "Do X\n\nHere is the analysis result that should appear in deliverables." + } + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + assert "Do X" not in summary + assert "analysis result" in summary + + # ========================================================================= + # Tool Tracking + # ========================================================================= + + def test_tracks_tools_used(self): + """Should track which tools were called.""" + messages = [ + { + "role": "assistant", + "content": "I will search for the information you requested using available tools.", + "tool_calls": [ + {"id": "1", "function": {"name": "web_search"}, "type": "function"}, + {"id": "2", "function": {"name": "read_file"}, "type": "function"}, + ] + }, + {"role": "tool", "content": "results", "tool_call_id": "1"}, + {"role": "tool", "content": "file content", "tool_call_id": "2"}, + { + "role": "assistant", + "content": "Based on my search and file reading, here are the comprehensive results of my analysis." + } + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + # Tools section should be present + assert "Tools Used" in summary + assert "web_search" in summary + assert "read_file" in summary + + # ========================================================================= + # Recency Priority + # ========================================================================= + + def test_prioritizes_recent_messages(self): + """Later messages (conclusions) should be prioritized over earlier ones.""" + messages = [ + {"role": "assistant", "content": "Early analysis: Starting to look at the problem and gathering initial data."}, + {"role": "assistant", "content": "Middle work: Processing the data and running various analyses on it."}, + {"role": "assistant", "content": "FINAL CONCLUSION: This is the definitive answer after all analysis is complete."}, + ] + + # With very tight budget, should get the last one (conclusion) + result = _extract_deliverables_from_messages( + messages, + summarization_budget_chars=200 + ) + summary = result.get("primary_summary", "") + + # Should contain the conclusion + assert "FINAL CONCLUSION" in summary + + # ========================================================================= + # Module Description + # ========================================================================= + + def test_includes_module_description(self): + """Module description should be included in output if provided.""" + messages = [ + {"role": "assistant", "content": "Here is my detailed analysis of the requested topic with findings."} + ] + + result = _extract_deliverables_from_messages( + messages, + module_description="Analyze user authentication flow" + ) + summary = result.get("primary_summary", "") + + assert "Analyze user authentication flow" in summary + assert "Work Module" in summary + + # ========================================================================= + # Metadata + # ========================================================================= + + def test_includes_extraction_metadata(self): + """Should include note about auto-extraction.""" + messages = [ + {"role": "assistant", "content": "Here is the analysis with substantive content for testing purposes."} + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + assert "Auto-extracted" in summary or "auto-extracted" in summary + + def test_notes_omitted_findings_count(self): + """When findings are omitted due to budget, should note how many.""" + # Create many findings + messages = [ + {"role": "assistant", "content": f"Analysis point {i}: " + "detailed content " * 20} + for i in range(10) + ] + + result = _extract_deliverables_from_messages( + messages, + summarization_budget_chars=500 + ) + summary = result.get("primary_summary", "") + + # Should mention omitted messages if any were skipped + if "omitted" in summary.lower(): + assert "earlier messages omitted" in summary.lower() or "messages omitted" in summary.lower() + + +class TestExtractDeliverablesEdgeCases: + """Edge case tests for extraction function.""" + + def test_message_without_content_key(self): + """Messages missing 'content' key should be handled.""" + messages = [ + {"role": "assistant"}, # No content key + {"role": "assistant", "content": "Valid content that should be extracted from messages."}, + ] + + result = _extract_deliverables_from_messages(messages) + # Should not crash, should extract valid content + assert "primary_summary" in result + + def test_tool_calls_without_function_key(self): + """Malformed tool_calls should not crash extraction.""" + messages = [ + { + "role": "assistant", + "content": "Calling a tool to get the necessary information for analysis.", + "tool_calls": [ + {"id": "1"}, # Missing function key + {"id": "2", "function": {}}, # Empty function + {"id": "3", "function": {"name": "valid_tool"}}, # Valid + ] + } + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + # Should extract content and track the valid tool + assert "valid_tool" in summary + + def test_handles_none_content(self): + """Content that is None should be handled.""" + messages = [ + {"role": "assistant", "content": None}, + {"role": "assistant", "content": "Valid content that should be extracted properly from this longer message."}, + ] + + result = _extract_deliverables_from_messages(messages) + # Only the valid message has content, and it's >50 chars, so should extract + assert "primary_summary" in result + + def test_max_findings_limit(self): + """Should have a reasonable max limit on findings to prevent extreme cases.""" + # Create 50 findings + messages = [ + {"role": "assistant", "content": f"Finding {i}: Detailed analysis content here." + "x" * 100} + for i in range(50) + ] + + result = _extract_deliverables_from_messages(messages) + summary = result.get("primary_summary", "") + + # Should not include all 50 - there's a max_findings_unbounded = 15 + finding_count = summary.count("### Finding") + assert finding_count <= 15 diff --git a/core/tests/test_handover_service.py b/core/tests/test_handover_service.py new file mode 100644 index 0000000..4b401be --- /dev/null +++ b/core/tests/test_handover_service.py @@ -0,0 +1,562 @@ +""" +Tier 3 Unit Tests for agent_core/framework/handover_service.py + +Tests the HandoverService class which manages agent handover protocols: +- Protocol loading and caching +- Schema extraction from tool parameters +- Path template resolution with placeholders +- Inheritance rule processing (direct, iterative, path-based) +- Message filtering with _no_handover flag +- Final payload and schema assembly + +Test Categories: +1. Protocol Loading: load_protocols(), get_protocol_schema() +2. Parameter Extraction: _extract_from_tool_params() +3. Path Resolution: _resolve_path() +4. Execution Flow: execute() - full handover execution +""" + +import pytest +from unittest.mock import patch, MagicMock, mock_open +import yaml + +# Import the class under test - need to handle the module-level load +with patch('agent_core.framework.handover_service.HandoverService.load_protocols'): + from agent_core.framework.handover_service import HandoverService + + +class TestHandoverServiceProtocolLoading: + """Tests for protocol loading and schema retrieval.""" + + def setup_method(self): + """Reset the class-level protocols cache before each test.""" + HandoverService._protocols = {} + + def test_load_protocols_empty_directory(self, tmp_path): + """Test loading from an empty directory.""" + with patch('agent_core.framework.handover_service.PROTOCOLS_DIR', tmp_path): + HandoverService._protocols = {} + HandoverService.load_protocols() + assert HandoverService._protocols == {} + + def test_load_protocols_valid_yaml(self, tmp_path): + """Test loading valid protocol YAML files.""" + protocol_data = { + "protocol_name": "test_protocol", + "context_parameters": {"type": "object", "properties": {"task": {"type": "string"}}}, + "inheritance": [], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + yaml_file = tmp_path / "test_protocol.yaml" + yaml_file.write_text(yaml.dump(protocol_data)) + + with patch('agent_core.framework.handover_service.PROTOCOLS_DIR', tmp_path): + HandoverService._protocols = {} + HandoverService.load_protocols() + + assert "test_protocol" in HandoverService._protocols + assert HandoverService._protocols["test_protocol"]["protocol_name"] == "test_protocol" + + def test_load_protocols_skips_if_already_loaded(self, tmp_path): + """Test that load_protocols skips if protocols already exist.""" + HandoverService._protocols = {"existing": {"protocol_name": "existing"}} + + with patch('agent_core.framework.handover_service.PROTOCOLS_DIR', tmp_path): + # Create a new file that shouldn't be loaded + yaml_file = tmp_path / "new.yaml" + yaml_file.write_text(yaml.dump({"protocol_name": "new"})) + + HandoverService.load_protocols() + + # Should still only have the existing protocol + assert "new" not in HandoverService._protocols + assert "existing" in HandoverService._protocols + + def test_load_protocols_handles_invalid_yaml(self, tmp_path): + """Test handling of invalid YAML files.""" + yaml_file = tmp_path / "invalid.yaml" + yaml_file.write_text("invalid: yaml: content: {[") + + with patch('agent_core.framework.handover_service.PROTOCOLS_DIR', tmp_path): + with patch('agent_core.framework.handover_service.logger'): + HandoverService._protocols = {} + # Should not raise, just log error + HandoverService.load_protocols() + assert HandoverService._protocols == {} + + def test_load_protocols_skips_missing_protocol_name(self, tmp_path): + """Test that files without protocol_name are skipped.""" + protocol_data = {"some_key": "some_value"} # No protocol_name + yaml_file = tmp_path / "no_name.yaml" + yaml_file.write_text(yaml.dump(protocol_data)) + + with patch('agent_core.framework.handover_service.PROTOCOLS_DIR', tmp_path): + HandoverService._protocols = {} + HandoverService.load_protocols() + assert HandoverService._protocols == {} + + def test_get_protocol_schema_existing(self): + """Test retrieving schema from existing protocol.""" + schema = {"type": "object", "properties": {"task": {"type": "string"}}} + HandoverService._protocols = { + "my_protocol": { + "protocol_name": "my_protocol", + "context_parameters": schema + } + } + + result = HandoverService.get_protocol_schema("my_protocol") + assert result == schema + + def test_get_protocol_schema_nonexistent(self): + """Test retrieving schema from non-existent protocol returns None.""" + HandoverService._protocols = {} + result = HandoverService.get_protocol_schema("nonexistent") + assert result is None + + def test_get_protocol_schema_no_context_params(self): + """Test protocol without context_parameters returns None.""" + HandoverService._protocols = { + "bare_protocol": {"protocol_name": "bare_protocol"} + } + result = HandoverService.get_protocol_schema("bare_protocol") + assert result is None + + +class TestExtractFromToolParams: + """Tests for _extract_from_tool_params method.""" + + def test_extract_matching_properties(self): + """Test extracting properties that exist in tool_params.""" + schema = { + "type": "object", + "properties": { + "task_description": {"type": "string"}, + "priority": {"type": "integer"} + } + } + tool_params = { + "task_description": "Do something", + "priority": 5, + "extra_param": "ignored" + } + + result = HandoverService._extract_from_tool_params(schema, tool_params) + + assert result == {"task_description": "Do something", "priority": 5} + assert "extra_param" not in result + + def test_extract_partial_match(self): + """Test extraction when only some properties are present.""" + schema = { + "type": "object", + "properties": { + "required_field": {"type": "string"}, + "optional_field": {"type": "string"} + } + } + tool_params = {"required_field": "value"} + + result = HandoverService._extract_from_tool_params(schema, tool_params) + + assert result == {"required_field": "value"} + assert "optional_field" not in result + + def test_extract_non_object_schema(self): + """Test with non-object type schema.""" + schema = {"type": "array", "items": {"type": "string"}} + tool_params = {"some": "params"} + + result = HandoverService._extract_from_tool_params(schema, tool_params) + assert result == {} + + def test_extract_no_properties(self): + """Test with object schema but no properties.""" + schema = {"type": "object"} + tool_params = {"some": "params"} + + result = HandoverService._extract_from_tool_params(schema, tool_params) + assert result == {} + + def test_extract_empty_tool_params(self): + """Test extraction from empty tool_params.""" + schema = { + "type": "object", + "properties": {"field": {"type": "string"}} + } + + result = HandoverService._extract_from_tool_params(schema, {}) + assert result == {} + + +class TestResolvePath: + """Tests for _resolve_path method.""" + + def test_resolve_single_placeholder(self): + """Test resolving a path with a single placeholder.""" + path_template = "state.modules.{{ module_id }}.data" + replacements = {"module_id": "meta.current_module"} + source_context = {"meta": {"current_module": "mod_123"}} + + result = HandoverService._resolve_path(path_template, replacements, source_context) + + assert result == "state.modules.mod_123.data" + + def test_resolve_multiple_placeholders(self): + """Test resolving a path with multiple placeholders.""" + path_template = "state.{{ agent }}.{{ action }}.result" + replacements = { + "agent": "meta.agent_id", + "action": "state.current_action_id" + } + source_context = { + "meta": {"agent_id": "principal"}, + "state": {"current_action_id": "search"} + } + + result = HandoverService._resolve_path(path_template, replacements, source_context) + + assert result == "state.principal.search.result" + + def test_resolve_unresolvable_placeholder_returns_none(self): + """Test that unresolvable placeholders return None.""" + path_template = "state.{{ missing }}.data" + replacements = {"missing": "nonexistent.path"} + source_context = {"state": {}} + + result = HandoverService._resolve_path(path_template, replacements, source_context) + + assert result is None + + def test_resolve_partial_resolution_returns_none(self): + """Test that partial resolution (leftover placeholders) returns None.""" + path_template = "state.{{ first }}.{{ second }}.data" + replacements = {"first": "meta.value"} + source_context = {"meta": {"value": "resolved"}} + + # second placeholder has no replacement + result = HandoverService._resolve_path(path_template, replacements, source_context) + + # Should still have {{ second }} unresolved, so returns None + assert result is None + + def test_resolve_no_placeholders(self): + """Test path with no placeholders.""" + path_template = "state.fixed.path" + replacements = {} + source_context = {} + + result = HandoverService._resolve_path(path_template, replacements, source_context) + + assert result == "state.fixed.path" + + +class TestHandoverServiceExecute: + """Tests for the execute() async method.""" + + def setup_method(self): + """Reset protocols cache before each test.""" + HandoverService._protocols = {} + + @pytest.mark.asyncio + async def test_execute_protocol_not_found(self): + """Test execute raises ValueError for unknown protocol.""" + HandoverService._protocols = {} + + with pytest.raises(ValueError, match="not found"): + await HandoverService.execute("unknown_protocol", {}) + + @pytest.mark.asyncio + async def test_execute_basic_protocol(self): + """Test execute with a simple protocol (no inheritance).""" + HandoverService._protocols = { + "simple_protocol": { + "protocol_name": "simple_protocol", + "context_parameters": { + "type": "object", + "properties": {"task": {"type": "string"}} + }, + "inheritance": [], + "target_inbox_item": {"source": "AGENT_STARTUP_BRIEFING"} + } + } + + source_context = { + "state": { + "current_action": {"task": "Test task", "type": "handoff"} + } + } + + result = await HandoverService.execute("simple_protocol", source_context) + + assert result["source"] == "AGENT_STARTUP_BRIEFING" + assert result["payload"]["data"]["task"] == "Test task" + assert "schema_for_rendering" in result["payload"] + + @pytest.mark.asyncio + async def test_execute_with_inheritance_condition_true(self): + """Test execute with inheritance rules that pass condition.""" + HandoverService._protocols = { + "conditional_protocol": { + "protocol_name": "conditional_protocol", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "len(v['state.messages']) > 0", + "from_source": {"path": "state.messages", "replace": {}}, + "as_payload_key": "inherited_messages", + "x-handover-title": "History" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {}, + "messages": [{"role": "user", "content": "Hello"}] + } + } + + result = await HandoverService.execute("conditional_protocol", source_context) + + assert "inherited_messages" in result["payload"]["data"] + assert len(result["payload"]["data"]["inherited_messages"]) == 1 + + @pytest.mark.asyncio + async def test_execute_with_inheritance_condition_false(self): + """Test execute with inheritance rules that fail condition.""" + HandoverService._protocols = { + "conditional_protocol": { + "protocol_name": "conditional_protocol", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "len(v['state.messages']) > 0", + "from_source": {"path": "state.messages", "replace": {}}, + "as_payload_key": "inherited_messages" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {}, + "messages": [] # Empty - condition will fail + } + } + + result = await HandoverService.execute("conditional_protocol", source_context) + + assert "inherited_messages" not in result["payload"]["data"] + + @pytest.mark.asyncio + async def test_execute_filters_no_handover_messages(self): + """Test that messages with _no_handover flag are filtered.""" + HandoverService._protocols = { + "filter_protocol": { + "protocol_name": "filter_protocol", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "True", + "from_source": {"path": "state.messages", "replace": {}}, + "as_payload_key": "inherited_messages" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {}, + "messages": [ + {"role": "user", "content": "Keep me"}, + {"role": "assistant", "content": "Filter me", "_internal": {"_no_handover": True}}, + {"role": "user", "content": "Keep me too"} + ] + } + } + + result = await HandoverService.execute("filter_protocol", source_context) + + inherited = result["payload"]["data"]["inherited_messages"] + assert len(inherited) == 2 + assert all(not msg.get("_internal", {}).get("_no_handover") for msg in inherited) + + @pytest.mark.asyncio + async def test_execute_with_path_replacement(self): + """Test execute with path resolution using replace.""" + HandoverService._protocols = { + "path_protocol": { + "protocol_name": "path_protocol", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "True", + "from_source": { + "path": "state.modules.{{ mod_id }}.data", + "replace": {"mod_id": "meta.current_module"} + }, + "as_payload_key": "module_data" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {}, + "modules": { + "active_module": {"data": {"key": "value"}} + } + }, + "meta": {"current_module": "active_module"} + } + + result = await HandoverService.execute("path_protocol", source_context) + + assert result["payload"]["data"]["module_data"] == {"key": "value"} + + @pytest.mark.asyncio + async def test_execute_with_iterative_inheritance(self): + """Test execute with iterative inheritance (path_to_iterate).""" + HandoverService._protocols = { + "iterative_protocol": { + "protocol_name": "iterative_protocol", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "True", + "from_source": { + "path_to_iterate": "state.items.{{ item_id }}.results", + "iterate_on": {"item_id": "meta.item_ids"} + }, + "as_payload_key": "all_results" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {}, + "items": { + "item_1": {"results": [{"result": "A"}]}, + "item_2": {"results": [{"result": "B"}, {"result": "C"}]} + } + }, + "meta": {"item_ids": ["item_1", "item_2"]} + } + + result = await HandoverService.execute("iterative_protocol", source_context) + + all_results = result["payload"]["data"]["all_results"] + assert len(all_results) == 3 + assert {"result": "A"} in all_results + assert {"result": "B"} in all_results + assert {"result": "C"} in all_results + + @pytest.mark.asyncio + async def test_execute_schema_for_rendering_populated(self): + """Test that schema_for_rendering is properly populated.""" + HandoverService._protocols = { + "schema_protocol": { + "protocol_name": "schema_protocol", + "context_parameters": { + "type": "object", + "properties": { + "task": {"type": "string", "description": "The task"} + } + }, + "inheritance": [ + { + "condition": "True", + "from_source": {"path": "state.history", "replace": {}}, + "as_payload_key": "context_history", + "x-handover-title": "Previous Context", + "schema": {"type": "array"} + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": { + "current_action": {"task": "Do something"}, + "history": ["event1", "event2"] + } + } + + result = await HandoverService.execute("schema_protocol", source_context) + + schema = result["payload"]["schema_for_rendering"] + assert schema["type"] == "object" + assert "task" in schema["properties"] + assert "context_history" in schema["properties"] + assert schema["properties"]["context_history"]["x-handover-title"] == "Previous Context" + + @pytest.mark.asyncio + async def test_execute_invalid_condition_continues(self): + """Test that invalid eval conditions are handled gracefully.""" + HandoverService._protocols = { + "bad_condition": { + "protocol_name": "bad_condition", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "this is not valid python", + "from_source": {"path": "state.data", "replace": {}}, + "as_payload_key": "data" + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = { + "state": {"current_action": {}, "data": "some data"} + } + + # Should not raise, just skip the rule + result = await HandoverService.execute("bad_condition", source_context) + + assert "data" not in result["payload"]["data"] + + @pytest.mark.asyncio + async def test_execute_missing_from_source_or_payload_key(self): + """Test that rules without from_source or as_payload_key are skipped.""" + HandoverService._protocols = { + "incomplete_rule": { + "protocol_name": "incomplete_rule", + "context_parameters": {"type": "object", "properties": {}}, + "inheritance": [ + { + "condition": "True", + "from_source": {"path": "state.data", "replace": {}} + # Missing as_payload_key + }, + { + "condition": "True", + "as_payload_key": "orphan_key" + # Missing from_source + } + ], + "target_inbox_item": {"source": "TEST_SOURCE"} + } + } + + source_context = {"state": {"current_action": {}, "data": "test"}} + + result = await HandoverService.execute("incomplete_rule", source_context) + + # Neither should be in the payload + assert "data" not in result["payload"]["data"] + assert "orphan_key" not in result["payload"]["data"] diff --git a/core/tests/test_inbox_processor.py b/core/tests/test_inbox_processor.py new file mode 100644 index 0000000..eecc49b --- /dev/null +++ b/core/tests/test_inbox_processor.py @@ -0,0 +1,571 @@ +""" +Unit tests for agent_core/framework/inbox_processor.py + +Tests the InboxProcessor class for processing inbox items into LLM messages. +""" + +import pytest +from unittest.mock import patch, MagicMock, AsyncMock +from datetime import datetime, timezone +import uuid + +from agent_core.framework.inbox_processor import InboxProcessor + + +@pytest.fixture +def basic_profile(): + """Basic agent profile for testing.""" + return { + "name": "TestAgent", + "llm_config_ref": "default", + "inbox_handling_strategies": [] + } + + +@pytest.fixture +def basic_context(): + """Basic context dictionary for testing.""" + return { + "state": { + "inbox": [], + "messages": [] + }, + "refs": { + "team": {"turns": []}, + "run": { + "runtime": { + "turn_manager": None, + "knowledge_base": None + }, + "config": { + "shared_llm_configs_ref": {} + } + } + }, + "meta": { + "agent_id": "test_agent", + "run_id": "test_run" + } + } + + +class TestInboxProcessorInit: + """Tests for InboxProcessor initialization.""" + + def test_initializes_with_profile_and_context(self, basic_profile, basic_context): + """Test processor initializes with correct attributes.""" + processor = InboxProcessor(basic_profile, basic_context) + + assert processor.profile == basic_profile + assert processor.context == basic_context + assert processor.agent_id == "test_agent" + + def test_extracts_state_references(self, basic_profile, basic_context): + """Test processor correctly extracts state references.""" + processor = InboxProcessor(basic_profile, basic_context) + + assert processor.state == basic_context["state"] + assert processor.team_state == basic_context["refs"]["team"] + assert processor.run_context == basic_context["refs"]["run"] + + +class TestCreateUserTurnFromInboxItem: + """Tests for _create_user_turn_from_inbox_item method.""" + + def test_creates_user_turn_for_user_prompt(self, basic_profile, basic_context): + """Test creates a user turn from USER_PROMPT item.""" + processor = InboxProcessor(basic_profile, basic_context) + + item = { + "source": "USER_PROMPT", + "payload": {"prompt": "Hello, agent!"}, + "metadata": {"created_at": "2024-01-01T00:00:00Z"} + } + + turn_id = processor._create_user_turn_from_inbox_item(item) + + assert turn_id is not None + assert turn_id.startswith("turn_user_") + + # Check turn was added to team_state + turns = basic_context["refs"]["team"]["turns"] + assert len(turns) == 1 + assert turns[0]["turn_type"] == "user_turn" + assert turns[0]["inputs"]["prompt"] == "Hello, agent!" + + def test_returns_none_for_empty_prompt(self, basic_profile, basic_context): + """Test returns None when payload has no prompt.""" + processor = InboxProcessor(basic_profile, basic_context) + + item = {"source": "USER_PROMPT", "payload": {}} + + turn_id = processor._create_user_turn_from_inbox_item(item) + + assert turn_id is None + + def test_links_to_previous_turn(self, basic_profile, basic_context): + """Test new user turn links to previous agent turn.""" + # Set up previous turn + basic_context["state"]["last_turn_id"] = "turn_agent_123" + basic_context["refs"]["team"]["turns"] = [{ + "turn_id": "turn_agent_123", + "flow_id": "flow_existing" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + item = { + "source": "USER_PROMPT", + "payload": {"prompt": "Follow up"}, + "metadata": {} + } + + turn_id = processor._create_user_turn_from_inbox_item(item) + + # Check new turn links to previous + new_turn = basic_context["refs"]["team"]["turns"][-1] + assert new_turn["source_turn_ids"] == ["turn_agent_123"] + assert new_turn["flow_id"] == "flow_existing" + + +class TestShouldDehydrateToolResult: + """Tests for _should_dehydrate_tool_result method.""" + + def test_returns_false_for_empty_content(self, basic_profile, basic_context): + """Test returns False when content is empty.""" + processor = InboxProcessor(basic_profile, basic_context) + + payload = {"content": "", "tool_name": "test"} + + result = processor._should_dehydrate_tool_result(payload) + + assert result is False + + def test_returns_true_for_large_content(self, basic_profile, basic_context): + """Test returns True when content exceeds 1KB.""" + processor = InboxProcessor(basic_profile, basic_context) + + large_content = "x" * 2000 # 2KB + payload = {"content": large_content, "tool_name": "test"} + + result = processor._should_dehydrate_tool_result(payload) + + assert result is True + + def test_returns_true_for_dehydrate_tools(self, basic_profile, basic_context): + """Test returns True for tools in dehydrate list.""" + processor = InboxProcessor(basic_profile, basic_context) + + for tool_name in ["web_search", "jina_visit", "jina_search"]: + payload = {"content": "small", "tool_name": tool_name} + + result = processor._should_dehydrate_tool_result(payload) + + assert result is True, f"Expected True for {tool_name}" + + def test_returns_false_for_small_normal_tool(self, basic_profile, basic_context): + """Test returns False for small content from normal tool.""" + processor = InboxProcessor(basic_profile, basic_context) + + payload = {"content": "small result", "tool_name": "normal_tool"} + + result = processor._should_dehydrate_tool_result(payload) + + assert result is False + + +class TestGetDehydrationReason: + """Tests for _get_dehydration_reason method.""" + + def test_returns_size_threshold_for_large_content(self, basic_profile, basic_context): + """Test returns 'size_threshold' for large content.""" + processor = InboxProcessor(basic_profile, basic_context) + + payload = {"content": "x" * 2000} + + result = processor._get_dehydration_reason(payload) + + assert result == "size_threshold" + + def test_returns_tool_policy_for_special_tools(self, basic_profile, basic_context): + """Test returns 'tool_policy' for special tools.""" + processor = InboxProcessor(basic_profile, basic_context) + + payload = {"content": "small", "tool_name": "web_search"} + + result = processor._get_dehydration_reason(payload) + + assert result == "tool_policy" + + def test_returns_unknown_for_other_cases(self, basic_profile, basic_context): + """Test returns 'unknown' for other dehydration cases.""" + processor = InboxProcessor(basic_profile, basic_context) + + payload = {"content": "small", "tool_name": "dispatch_submodules"} + + result = processor._get_dehydration_reason(payload) + + assert result == "unknown" + + +class TestInboxProcessorProcess: + """Tests for the main process method.""" + + @pytest.mark.asyncio + async def test_returns_existing_messages_when_inbox_empty(self, basic_profile, basic_context): + """Test returns existing messages when inbox is empty.""" + basic_context["state"]["messages"] = [{"role": "user", "content": "Hi"}] + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + assert result["messages_for_llm"] == [{"role": "user", "content": "Hi"}] + assert result["processing_log"] == [] + assert result["processed_item_ids"] == [] + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_processes_single_inbox_item(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test processes a single inbox item.""" + # Setup mocks + mock_ingestor.return_value = "Processed content" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "item_1", + "source": "INTERNAL_DIRECTIVE", + "payload": {"message": "Do something"}, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + assert len(result["messages_for_llm"]) == 1 + assert "item_1" in result["processed_item_ids"] + assert len(result["processing_log"]) == 1 + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_sorts_inbox_by_priority(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test inbox items are sorted by priority.""" + mock_ingestor.return_value = "Content" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [ + {"item_id": "user", "source": "USER_PROMPT", "payload": {"prompt": "Hi"}, "consumption_policy": "consume_on_read"}, + {"item_id": "tool", "source": "TOOL_RESULT", "payload": {"content": "Result"}, "consumption_policy": "consume_on_read"}, + ] + + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + # TOOL_RESULT (priority 0) should be processed before USER_PROMPT (priority 100) + assert result["processed_item_ids"][0] == "tool" + assert result["processed_item_ids"][1] == "user" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_keeps_persistent_items(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test persistent items are kept in inbox.""" + mock_ingestor.return_value = "Content" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [ + {"item_id": "persistent", "source": "TEST", "payload": {}, "consumption_policy": "persistent_until_consumed"}, + {"item_id": "consumed", "source": "TEST", "payload": {}, "consumption_policy": "consume_on_read"}, + ] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + # Only persistent item should remain + remaining = basic_context["state"]["inbox"] + assert len(remaining) == 1 + assert remaining[0]["item_id"] == "persistent" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_expires_old_persistent_items(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test persistent items expire based on max_turns_in_inbox.""" + mock_ingestor.return_value = "Content" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "expiring", + "source": "TEST", + "payload": {}, + "consumption_policy": "persistent_until_consumed", + "metadata": { + "max_turns_in_inbox": 2, + "turn_count_in_inbox": 2 # Already at limit + } + }] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + # Item should be expired (not in remaining inbox) + assert len(basic_context["state"]["inbox"]) == 0 + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_sets_startup_briefing_flag(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test AGENT_STARTUP_BRIEFING sets initial_briefing_delivered flag.""" + mock_ingestor.return_value = "Briefing content" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "briefing", + "source": "AGENT_STARTUP_BRIEFING", + "payload": {"content": "Welcome"}, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + assert basic_context["state"]["flags"]["initial_briefing_delivered"] is True + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_handles_ingestor_exception(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test handles exceptions during ingestion gracefully.""" + mock_ingestor.side_effect = Exception("Ingestor failed") + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "failing", + "source": "TEST", + "payload": {}, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + # Should add error message to LLM messages + assert len(result["messages_for_llm"]) == 1 + assert "system_error" in result["messages_for_llm"][0]["content"] + assert result["messages_for_llm"][0]["role"] == "system" + + +class TestInboxProcessorToolResults: + """Tests for TOOL_RESULT processing.""" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.INGESTOR_REGISTRY", {}) + @patch("agent_core.framework.inbox_processor.EVENT_STRATEGY_REGISTRY", {}) + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_creates_tool_message_format(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test TOOL_RESULT creates message with tool role.""" + mock_ingestor.return_value = "Tool output" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "tool_result_1", + "source": "TOOL_RESULT", + "payload": { + "content": "Search results", + "tool_call_id": "call_123", + "tool_name": "web_search" + }, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + messages = result["messages_for_llm"] + assert len(messages) == 1 + # The actual role depends on the ingestor params + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_updates_turn_manager_on_tool_result(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test TOOL_RESULT updates turn manager.""" + mock_ingestor.return_value = "Tool output" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + # Setup mock turn manager + mock_turn_manager = MagicMock() + basic_context["refs"]["run"]["runtime"]["turn_manager"] = mock_turn_manager + + basic_context["state"]["inbox"] = [{ + "item_id": "tool_result_1", + "source": "TOOL_RESULT", + "payload": { + "content": "Result", + "tool_call_id": "call_123", + "tool_name": "test_tool", + "is_error": False + }, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + mock_turn_manager.update_tool_interaction_result.assert_called_once() + + +class TestInboxProcessorUserPrompt: + """Tests for USER_PROMPT processing.""" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_creates_user_turn_for_user_prompt(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test USER_PROMPT creates a user turn.""" + mock_ingestor.return_value = "User message" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "user_msg", + "source": "USER_PROMPT", + "payload": {"prompt": "Hello!"}, + "consumption_policy": "consume_on_read", + "metadata": {} + }] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + # Check user turn was created + turns = basic_context["refs"]["team"]["turns"] + assert len(turns) == 1 + assert turns[0]["turn_type"] == "user_turn" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.markdown_formatter_ingestor") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_updates_last_turn_id_after_user_prompt(self, mock_resolver, mock_ingestor, basic_profile, basic_context): + """Test last_turn_id is updated after processing USER_PROMPT.""" + mock_ingestor.return_value = "User message" + mock_ingestor.__name__ = "markdown_formatter_ingestor" + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + basic_context["state"]["inbox"] = [{ + "item_id": "user_msg", + "source": "USER_PROMPT", + "payload": {"prompt": "Hello!"}, + "consumption_policy": "consume_on_read", + "metadata": {} + }] + + processor = InboxProcessor(basic_profile, basic_context) + + await processor.process() + + # last_turn_id should be set to the new user turn + assert basic_context["state"]["last_turn_id"].startswith("turn_user_") + + +class TestInboxProcessorStrategySelection: + """Tests for handling strategy selection.""" + + @pytest.mark.asyncio + @patch("agent_core.framework.inbox_processor.INGESTOR_REGISTRY") + @patch("agent_core.framework.inbox_processor.LLMConfigResolver") + async def test_uses_profile_strategy_override(self, mock_resolver, mock_ingestor_registry, basic_profile, basic_context): + """Test uses profile-defined strategy when available.""" + custom_ingestor = MagicMock(return_value="Custom processed") + custom_ingestor.__name__ = "custom_ingestor" + mock_ingestor_registry.get.return_value = custom_ingestor + + mock_resolver_instance = MagicMock() + mock_resolver_instance.resolve.return_value = {"model": "gpt-4"} + mock_resolver.return_value = mock_resolver_instance + + # Add strategy to profile + basic_profile["inbox_handling_strategies"] = [{ + "source": "CUSTOM_SOURCE", + "ingestor": "custom_ingestor", + "injection_mode": "append_as_new_message", + "params": {"role": "user"} + }] + + basic_context["state"]["inbox"] = [{ + "item_id": "custom_item", + "source": "CUSTOM_SOURCE", + "payload": {"data": "test"}, + "consumption_policy": "consume_on_read" + }] + + processor = InboxProcessor(basic_profile, basic_context) + + result = await processor.process() + + # Should use custom ingestor + assert result["processing_log"][0]["handling_strategy_source"] == "profile" + + +class TestInboxProcessorPriorityOrder: + """Tests for inbox priority ordering.""" + + def test_priority_map_values(self, basic_profile, basic_context): + """Test priority map has expected ordering.""" + # We can't access the private priority_map directly, + # but we can test the sorting behavior + basic_context["state"]["inbox"] = [ + {"item_id": "1", "source": "USER_PROMPT", "payload": {}}, # 100 + {"item_id": "2", "source": "TOOL_RESULT", "payload": {}}, # 0 + {"item_id": "3", "source": "INTERNAL_DIRECTIVE", "payload": {}}, # 15 + {"item_id": "4", "source": "AGENT_STARTUP_BRIEFING", "payload": {}}, # 8 + ] + + processor = InboxProcessor(basic_profile, basic_context) + + # Process to trigger sorting (items are sorted in process()) + # We can check the order by examining after first sort + inbox = basic_context["state"]["inbox"] + + # After processing, inbox should be sorted + # TOOL_RESULT < AGENT_STARTUP_BRIEFING < INTERNAL_DIRECTIVE < USER_PROMPT + # Note: actual sorting happens inside process(), testing indirectly diff --git a/core/tests/test_ingestors.py b/core/tests/test_ingestors.py new file mode 100644 index 0000000..6a93e80 --- /dev/null +++ b/core/tests/test_ingestors.py @@ -0,0 +1,256 @@ +""" +Tests for ingestors.py - specifically the EXCLUDED_FIELDS filtering. + +These tests ensure that large fields like context_archive are properly filtered +out during work_modules injection to prevent context window explosion. +""" +import pytest +import sys +from pathlib import Path + +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + +from agent_core.events.ingestors import ( + work_modules_ingestor, + INGESTOR_REGISTRY, +) + + +class TestWorkModulesIngestorExcludedFields: + """Tests for EXCLUDED_FIELDS filtering in work_modules_ingestor.""" + + def test_excludes_context_archive(self): + """context_archive should be completely excluded from output.""" + payload = { + "WM_1": { + "name": "Test Module", + "status": "pending_review", + "context_archive": [ + { + "messages": [{"role": "assistant", "content": "x" * 10000}], + "deliverables": {"primary_summary": "test"}, + "model": "claude-sonnet-4-20250514" + } + ] + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # context_archive should not appear + assert "context_archive" not in result + # But other fields should + assert "Test Module" in result + assert "pending_review" in result + + def test_excludes_messages_field(self): + """messages field should be excluded.""" + payload = { + "WM_1": { + "name": "Test", + "status": "active", + "messages": [ + {"role": "user", "content": "old message " * 1000} + ] + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # Should not contain the messages content + assert "old message" not in result + # But module name should be there + assert "Test" in result + + def test_excludes_raw_messages(self): + """raw_messages field should be excluded.""" + payload = { + "WM_1": { + "name": "Analysis", + "raw_messages": ["msg1", "msg2", "msg3"] + } + } + + result = work_modules_ingestor(payload, {}, {}) + + assert "raw_messages" not in result + assert "msg1" not in result + assert "Analysis" in result + + def test_excludes_full_context(self): + """full_context field should be excluded.""" + payload = { + "WM_1": { + "name": "Work Item", + "full_context": {"massive": "data " * 5000} + } + } + + result = work_modules_ingestor(payload, {}, {}) + + assert "full_context" not in result + assert "massive" not in result + assert "Work Item" in result + + def test_excludes_new_messages_from_associate(self): + """new_messages_from_associate should be excluded.""" + payload = { + "WM_1": { + "name": "Task", + "new_messages_from_associate": [ + {"role": "assistant", "content": "associate work " * 500} + ] + } + } + + result = work_modules_ingestor(payload, {}, {}) + + assert "new_messages_from_associate" not in result + assert "associate work" not in result + assert "Task" in result + + def test_all_excluded_fields_together(self): + """Multiple excluded fields should all be filtered out.""" + payload = { + "WM_1": { + "name": "Complete Module", + "description": "A test module", + "status": "completed", + # All excluded fields: + "context_archive": [{"messages": ["large"]}], + "full_context": {"big": "data"}, + "raw_messages": ["msg"], + "messages": [{"role": "user", "content": "x"}], + "new_messages_from_associate": [{"role": "assistant", "content": "y"}], + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # None of the excluded fields should appear + assert "context_archive" not in result + assert "full_context" not in result + assert "raw_messages" not in result + # Note: "messages" might appear as a word in formatting, + # but the actual message content shouldn't + + # Valid fields should appear + assert "Complete Module" in result + assert "test module" in result.lower() + assert "completed" in result + + +class TestWorkModulesIngestorSummarizeFields: + """Tests for SUMMARIZE_FIELDS handling.""" + + def test_summarizes_deliverables_dict(self): + """deliverables dict should be summarized as count.""" + payload = { + "WM_1": { + "name": "Test", + "deliverables": { + "primary_summary": "summary", + "key_findings": ["a", "b"], + "recommendations": "do this" + } + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # Should show count, not full content + assert "(3 items)" in result + # Full content should not be dumped + assert "do this" not in result + + def test_summarizes_tools_used(self): + """tools_used should be summarized with truncation for long lists.""" + payload = { + "WM_1": { + "name": "Test", + "tools_used": ["tool1", "tool2", "tool3", "tool4", "tool5", "tool6", "tool7"] + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # Should show first 5 tools + assert "tool1" in result + assert "tool5" in result + # Should indicate truncation + assert "..." in result + + +class TestWorkModulesIngestorEdgeCases: + """Edge case tests.""" + + def test_handles_empty_payload(self): + """Empty payload should return appropriate message.""" + result = work_modules_ingestor({}, {}, {}) + + assert "No work modules" in result + + def test_handles_none_payload_equivalent(self): + """Non-dict payload should be handled gracefully.""" + result = work_modules_ingestor("invalid", {}, {}) + + assert "not in the expected format" in result + + def test_handles_nested_large_dicts(self): + """Large nested dicts should be filtered recursively.""" + payload = { + "WM_1": { + "name": "Test", + "nested": { + "context_archive": ["should be excluded"], + "valid_field": "should remain" + } + } + } + + result = work_modules_ingestor(payload, {}, {}) + + # The nested excluded field should be filtered + # The valid nested field should remain + assert "valid_field" in result or "should remain" in result + + def test_preserves_small_fields(self): + """Small fields that aren't in exclude list should be preserved.""" + payload = { + "WM_1": { + "name": "My Module", + "description": "A description", + "status": "active", + "priority": "high", + "created_at": "2025-01-01", + "assignee": "Associate_1" + } + } + + result = work_modules_ingestor(payload, {}, {}) + + assert "My Module" in result + assert "description" in result.lower() + assert "active" in result + assert "high" in result + assert "2025-01-01" in result + assert "Associate_1" in result + + def test_custom_title_param(self): + """Should use custom title from params.""" + payload = {"WM_1": {"name": "Test"}} + params = {"title": "## Custom Work Modules Header"} + + result = work_modules_ingestor(payload, params, {}) + + assert "Custom Work Modules Header" in result + + +class TestIngestorRegistry: + """Test that ingestors are properly registered.""" + + def test_work_modules_ingestor_registered(self): + """work_modules_ingestor should be in the registry.""" + assert "work_modules_ingestor" in INGESTOR_REGISTRY + assert INGESTOR_REGISTRY["work_modules_ingestor"] == work_modules_ingestor diff --git a/core/tests/test_jina_api.py b/core/tests/test_jina_api.py new file mode 100644 index 0000000..c94c70b --- /dev/null +++ b/core/tests/test_jina_api.py @@ -0,0 +1,229 @@ +""" +Unit tests for agent_core.services.jina_api module. + +This module tests the Jina AI API helper functions for web search +and URL content retrieval. + +Key functionality tested: +- get_jina_key: Environment variable retrieval +- test_jina_search: Search API connectivity test +- test_jina_visit: URL visit API connectivity test +""" + +import pytest +import os +from unittest.mock import patch, MagicMock +from agent_core.services.jina_api import ( + get_jina_key, + test_jina_search, + test_jina_visit, +) + + +class TestGetJinaKey: + """Tests for get_jina_key function.""" + + def test_returns_key_when_set(self): + """Test returns API key when environment variable is set.""" + with patch.dict(os.environ, {"JINA_KEY": "test-api-key-123"}): + result = get_jina_key() + + assert result == "test-api-key-123" + + def test_returns_none_when_not_set(self): + """Test returns None when environment variable not set.""" + with patch.dict(os.environ, {}, clear=True): + result = get_jina_key() + + assert result is None + + def test_returns_empty_string_if_set_empty(self): + """Test returns empty string if env var is set to empty.""" + with patch.dict(os.environ, {"JINA_KEY": ""}): + result = get_jina_key() + + # Empty string is still falsy, but get_jina_key returns the value + # The function checks "if not jina_key" so empty returns None-like behavior + # Actually checking implementation: it returns jina_key regardless + # Let me verify - os.environ.get returns "" if set to "" + # The function then checks "if not jina_key" which is True for "" + # So it logs error and returns... actually it just returns jina_key + # Need to check actual implementation + assert result == "" + + +class TestJinaSearchAPI: + """Tests for test_jina_search function.""" + + def test_returns_false_when_no_api_key(self): + """Test returns False when API key not available.""" + with patch.dict(os.environ, {}, clear=True): + result = test_jina_search() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_true_on_success(self, mock_get): + """Test returns True when API call succeeds.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): + result = test_jina_search() + + assert result is True + mock_get.assert_called_once() + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_false_on_non_200_status(self, mock_get): + """Test returns False when API returns non-200 status.""" + mock_response = MagicMock() + mock_response.status_code = 401 # Unauthorized + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "invalid-key"}): + result = test_jina_search() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_false_on_exception(self, mock_get): + """Test returns False when request raises exception.""" + mock_get.side_effect = Exception("Network error") + + with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): + result = test_jina_search() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_uses_correct_url_format(self, mock_get): + """Test uses correct Jina search URL format.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "key"}): + test_jina_search(query="test query") + + call_url = mock_get.call_args[0][0] + assert "s.jina.ai" in call_url + assert "test query" in call_url + + @patch('agent_core.services.jina_api.requests.get') + def test_includes_auth_header(self, mock_get): + """Test includes authorization header.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "my-api-key"}): + test_jina_search() + + call_headers = mock_get.call_args[1]["headers"] + assert "Authorization" in call_headers + assert "Bearer my-api-key" in call_headers["Authorization"] + + +class TestJinaVisitAPI: + """Tests for test_jina_visit function.""" + + def test_returns_false_when_no_api_key(self): + """Test returns False when API key not available.""" + with patch.dict(os.environ, {}, clear=True): + result = test_jina_visit() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_true_on_success(self, mock_get): + """Test returns True when API call succeeds.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): + result = test_jina_visit() + + assert result is True + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_false_on_non_200_status(self, mock_get): + """Test returns False when API returns non-200 status.""" + mock_response = MagicMock() + mock_response.status_code = 500 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "key"}): + result = test_jina_visit() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_returns_false_on_exception(self, mock_get): + """Test returns False when request raises exception.""" + mock_get.side_effect = ConnectionError("Failed to connect") + + with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): + result = test_jina_visit() + + assert result is False + + @patch('agent_core.services.jina_api.requests.get') + def test_uses_correct_url_format(self, mock_get): + """Test uses correct Jina reader URL format.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "key"}): + test_jina_visit(url="example.com/page") + + call_url = mock_get.call_args[0][0] + assert "r.jina.ai" in call_url + assert "example.com/page" in call_url + + @patch('agent_core.services.jina_api.requests.get') + def test_includes_auth_header(self, mock_get): + """Test includes authorization header.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "secret-key"}): + test_jina_visit() + + call_headers = mock_get.call_args[1]["headers"] + assert "Authorization" in call_headers + assert "Bearer secret-key" in call_headers["Authorization"] + + @patch('agent_core.services.jina_api.requests.get') + def test_default_url_is_github(self, mock_get): + """Test default URL parameter is github.com.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "key"}): + test_jina_visit() # No url parameter + + call_url = mock_get.call_args[0][0] + assert "github.com" in call_url + + +class TestDefaultParameters: + """Tests for default parameter values.""" + + @patch('agent_core.services.jina_api.requests.get') + def test_search_default_query(self, mock_get): + """Test default search query is 'PocketFlow'.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + with patch.dict(os.environ, {"JINA_KEY": "key"}): + test_jina_search() # No query parameter + + call_url = mock_get.call_args[0][0] + assert "PocketFlow" in call_url diff --git a/core/tests/test_llm_utils.py b/core/tests/test_llm_utils.py new file mode 100644 index 0000000..b833157 --- /dev/null +++ b/core/tests/test_llm_utils.py @@ -0,0 +1,364 @@ +""" +Unit tests for agent_core.llm.utils module. + +This module tests utility functions for parsing LLM responses, +particularly the fallback tool call extraction from text content. + +Key functions tested: +- _parse_arguments_string: Safely parses Python-style arguments to dict +- extract_tool_calls_from_content: Extracts tool calls from blocks +""" + +import pytest +import json +from agent_core.llm.utils import ( + _parse_arguments_string, + extract_tool_calls_from_content, +) + + +class TestParseArgumentsString: + """Tests for the _parse_arguments_string helper function.""" + + def test_empty_string(self): + """Test parsing empty string returns empty dict.""" + result = _parse_arguments_string("", "test-agent") + assert result == {} + + def test_whitespace_only(self): + """Test parsing whitespace-only string returns empty dict.""" + result = _parse_arguments_string(" \n\t ", "test-agent") + assert result == {} + + def test_single_keyword_string_arg(self): + """Test parsing single keyword argument with string value.""" + result = _parse_arguments_string('query="hello world"', "test-agent") + assert result == {"query": "hello world"} + + def test_single_keyword_int_arg(self): + """Test parsing single keyword argument with integer value.""" + result = _parse_arguments_string("limit=10", "test-agent") + assert result == {"limit": 10} + + def test_single_keyword_float_arg(self): + """Test parsing single keyword argument with float value.""" + result = _parse_arguments_string("temperature=0.7", "test-agent") + assert result == {"temperature": 0.7} + + def test_single_keyword_bool_arg(self): + """Test parsing single keyword argument with boolean value.""" + result = _parse_arguments_string("verbose=True", "test-agent") + assert result == {"verbose": True} + + result = _parse_arguments_string("debug=False", "test-agent") + assert result == {"debug": False} + + def test_multiple_keyword_args(self): + """Test parsing multiple keyword arguments.""" + result = _parse_arguments_string('query="test", limit=5, debug=True', "test-agent") + assert result == {"query": "test", "limit": 5, "debug": True} + + def test_list_argument(self): + """Test parsing list as argument value.""" + result = _parse_arguments_string('items=["a", "b", "c"]', "test-agent") + assert result == {"items": ["a", "b", "c"]} + + def test_dict_argument(self): + """Test parsing dict as argument value.""" + result = _parse_arguments_string('config={"key": "value", "count": 1}', "test-agent") + assert result == {"config": {"key": "value", "count": 1}} + + def test_nested_structures(self): + """Test parsing nested list/dict structures.""" + result = _parse_arguments_string( + 'data={"nested": [1, 2, {"deep": True}]}', + "test-agent" + ) + assert result == {"data": {"nested": [1, 2, {"deep": True}]}} + + def test_none_value(self): + """Test parsing None as argument value.""" + result = _parse_arguments_string("value=None", "test-agent") + assert result == {"value": None} + + def test_string_with_special_chars(self): + """Test parsing strings with special characters.""" + result = _parse_arguments_string('text="Hello, world! How\'s it going?"', "test-agent") + assert result == {"text": "Hello, world! How's it going?"} + + def test_multiline_string(self): + """Test parsing multiline strings.""" + # Triple-quoted strings work in Python literals + result = _parse_arguments_string('text="""line1\nline2\nline3"""', "test-agent") + assert result == {"text": "line1\nline2\nline3"} + + def test_positional_args(self): + """Test parsing positional arguments (uncommon but supported).""" + result = _parse_arguments_string('"positional1", 42', "test-agent") + assert result == {"arg0": "positional1", "arg1": 42} + + def test_mixed_positional_and_keyword(self): + """Test parsing mixed positional and keyword arguments.""" + result = _parse_arguments_string('"first", key="second"', "test-agent") + assert result == {"arg0": "first", "key": "second"} + + def test_syntax_error_returns_empty(self): + """Test that syntax errors return empty dict.""" + result = _parse_arguments_string("invalid syntax here !!!", "test-agent") + assert result == {} + + def test_unbalanced_quotes_returns_empty(self): + """Test that unbalanced quotes return empty dict.""" + result = _parse_arguments_string('query="unclosed', "test-agent") + assert result == {} + + def test_complex_json_like_string(self): + """Test parsing complex JSON-like string argument.""" + json_str = '{"users": [{"name": "Alice"}, {"name": "Bob"}], "count": 2}' + result = _parse_arguments_string(f'data={json_str}', "test-agent") + assert result == {"data": {"users": [{"name": "Alice"}, {"name": "Bob"}], "count": 2}} + + +class TestExtractToolCallsFromContent: + """Tests for the extract_tool_calls_from_content function.""" + + def test_no_tool_calls(self): + """Test content with no tool calls returns empty list.""" + content = "This is just regular text without any tool calls." + result = extract_tool_calls_from_content(content, "test-agent") + assert result == [] + + def test_single_simple_tool_call(self): + """Test extracting a single simple tool call.""" + content = 'print(MyTool(query="hello"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["type"] == "function" + assert result[0]["function"]["name"] == "MyTool" + + args = json.loads(result[0]["function"]["arguments"]) + assert args == {"query": "hello"} + + def test_tool_call_with_multiple_args(self): + """Test extracting tool call with multiple arguments.""" + content = 'print(SearchTool(query="test", limit=10, verbose=True))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args == {"query": "test", "limit": 10, "verbose": True} + + def test_multiple_tool_calls(self): + """Test extracting multiple tool calls from content.""" + content = ''' + Let me search for that. + print(Search(query="AI")) + And also calculate. + print(Calculate(expression="2+2")) + ''' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 2 + assert result[0]["function"]["name"] == "Search" + assert result[1]["function"]["name"] == "Calculate" + + def test_tool_call_with_no_args(self): + """Test extracting tool call with no arguments.""" + content = 'print(GetStatus())' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["function"]["name"] == "GetStatus" + args = json.loads(result[0]["function"]["arguments"]) + assert args == {} + + def test_tool_call_with_complex_args(self): + """Test extracting tool call with complex nested arguments.""" + content = 'print(ProcessData(config={"nested": [1, 2, 3]}))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args == {"config": {"nested": [1, 2, 3]}} + + def test_tool_call_id_format(self): + """Test that tool_call_id follows expected format.""" + content = 'print(TestTool())' + result = extract_tool_calls_from_content(content, "my-agent") + + assert len(result) == 1 + # ID should start with "fallback_" followed by agent_id + assert result[0]["id"].startswith("fallback_my-agent_") + + def test_whitespace_in_tool_code_tags(self): + """Test that whitespace inside tool_code tags is handled.""" + content = ' print(MyTool(arg="value")) ' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["function"]["name"] == "MyTool" + + def test_newlines_in_tool_code_tags(self): + """Test that newlines inside tool_code tags are handled.""" + content = ''' + print(MyTool( + arg1="value1", + arg2="value2" + )) + ''' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["function"]["name"] == "MyTool" + args = json.loads(result[0]["function"]["arguments"]) + assert args["arg1"] == "value1" + assert args["arg2"] == "value2" + + def test_invalid_tool_call_format_skipped(self): + """Test that invalid tool call formats are skipped.""" + content = 'print()' # Empty print + result = extract_tool_calls_from_content(content, "test-agent") + # Should not extract anything or handle gracefully + assert isinstance(result, list) + + def test_malformed_tool_code_tag(self): + """Test that malformed tags don't cause errors.""" + content = 'print(Incomplete' # Unclosed tag + result = extract_tool_calls_from_content(content, "test-agent") + assert result == [] + + def test_tool_call_with_underscore_name(self): + """Test tool names with underscores.""" + content = 'print(my_tool_name(arg="test"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["function"]["name"] == "my_tool_name" + + def test_tool_call_mixed_with_text(self): + """Test tool calls embedded in regular text.""" + content = ''' + I'll help you with that. First, let me search: + print(Search(query="example")) + + That should give us the information we need. + ''' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + assert result[0]["function"]["name"] == "Search" + + def test_empty_content(self): + """Test with empty content.""" + result = extract_tool_calls_from_content("", "test-agent") + assert result == [] + + def test_tool_with_string_containing_parentheses(self): + """Test tool calls with strings containing parentheses.""" + content = 'print(Format(text="Hello (world)"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args == {"text": "Hello (world)"} + + +class TestToolCallStructure: + """Tests verifying the structure of extracted tool calls matches OpenAI format.""" + + def test_structure_matches_openai_format(self): + """Test that extracted tool calls match OpenAI's tool_calls format.""" + content = 'print(TestFunc(param="value"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + tool_call = result[0] + + # Must have 'id', 'type', 'function' keys + assert "id" in tool_call + assert "type" in tool_call + assert "function" in tool_call + + # Type must be "function" + assert tool_call["type"] == "function" + + # Function must have 'name' and 'arguments' + assert "name" in tool_call["function"] + assert "arguments" in tool_call["function"] + + # Arguments must be JSON string + assert isinstance(tool_call["function"]["arguments"], str) + parsed = json.loads(tool_call["function"]["arguments"]) + assert isinstance(parsed, dict) + + def test_arguments_is_valid_json_string(self): + """Test that arguments field is always valid JSON.""" + content = 'print(MyTool(a=1, b="two", c=[3, 4]))' + result = extract_tool_calls_from_content(content, "test-agent") + + args_str = result[0]["function"]["arguments"] + + # Should not raise + parsed = json.loads(args_str) + assert parsed["a"] == 1 + assert parsed["b"] == "two" + assert parsed["c"] == [3, 4] + + +class TestEdgeCasesAndRobustness: + """Tests for edge cases and error handling.""" + + def test_unicode_in_arguments(self): + """Test handling of unicode characters in arguments.""" + content = 'print(Translate(text="γ“γ‚“γ«γ‘γ―δΈ–η•Œ"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args["text"] == "γ“γ‚“γ«γ‘γ―δΈ–η•Œ" + + def test_emoji_in_arguments(self): + """Test handling of emoji in arguments.""" + content = 'print(Post(message="Hello πŸ‘‹ World 🌍"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args["message"] == "Hello πŸ‘‹ World 🌍" + + def test_very_long_argument(self): + """Test handling of very long argument values.""" + long_text = "x" * 10000 + content = f'print(Process(data="{long_text}"))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert len(args["data"]) == 10000 + + def test_special_agent_id_chars(self): + """Test with special characters in agent_id.""" + content = 'print(Test())' + result = extract_tool_calls_from_content(content, "agent-with-dashes_and_underscores") + + assert len(result) == 1 + assert "agent-with-dashes_and_underscores" in result[0]["id"] + + def test_consecutive_tool_calls(self): + """Test tool calls appearing consecutively without text between.""" + content = 'print(First())print(Second())' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 2 + assert result[0]["function"]["name"] == "First" + assert result[1]["function"]["name"] == "Second" + + def test_deeply_nested_arguments(self): + """Test handling of deeply nested argument structures.""" + content = 'print(Deep(data={"l1": {"l2": {"l3": {"l4": "deep"}}}}))' + result = extract_tool_calls_from_content(content, "test-agent") + + assert len(result) == 1 + args = json.loads(result[0]["function"]["arguments"]) + assert args["data"]["l1"]["l2"]["l3"]["l4"] == "deep" diff --git a/core/tests/test_mcp_reconnection.py b/core/tests/test_mcp_reconnection.py new file mode 100644 index 0000000..d30b00a --- /dev/null +++ b/core/tests/test_mcp_reconnection.py @@ -0,0 +1,289 @@ +""" +Unit tests for MCP reconnection functionality. + +Tests the automatic reconnection logic in MCPProxyNode when ClosedResourceError occurs. +""" + +import pytest +import asyncio +from unittest.mock import AsyncMock, MagicMock, patch +import anyio + + +class TestMCPReconnection: + """Tests for MCP server reconnection functionality.""" + + @pytest.fixture + def mock_session_group(self): + """Create a mock session group.""" + session_group = MagicMock() + session_group.sessions = [] + session_group.call_tool = AsyncMock() + session_group.connect_to_server = AsyncMock() + session_group.disconnect_from_server = AsyncMock() + return session_group + + @pytest.fixture + def mock_tool_result(self): + """Create a mock successful tool result.""" + content_item = MagicMock() + content_item.text = "Tool execution successful" + + result = MagicMock() + result.content = [content_item] + return result + + @pytest.fixture + def mcp_proxy_node(self): + """Create an MCPProxyNode instance for testing.""" + from agent_core.nodes.mcp_proxy_node import MCPProxyNode + + tool_info = { + "name": "test_tool", + "description": "A test tool", + "inputSchema": {"type": "object", "properties": {}} + } + + return MCPProxyNode( + unique_tool_name="TestServer_test_tool", + original_tool_name="test_tool", + server_name="TestServer", + tool_info=tool_info + ) + + @pytest.mark.asyncio + async def test_successful_call_no_reconnect_needed(self, mcp_proxy_node, mock_session_group, mock_tool_result): + """Test that successful calls don't trigger reconnection.""" + mock_session_group.call_tool.return_value = mock_tool_result + + prep_res = { + "tool_params": {"arg1": "value1"}, + "shared_context": { + "runtime_objects": {"mcp_session_group": mock_session_group} + } + } + + result = await mcp_proxy_node.exec_async(prep_res) + + assert result["status"] == "success" + assert "Tool execution successful" in result["payload"]["response_preview"] + mock_session_group.call_tool.assert_called_once() + + @pytest.mark.asyncio + async def test_reconnect_on_closed_resource_error(self, mcp_proxy_node, mock_session_group, mock_tool_result): + """Test that ClosedResourceError triggers reconnection.""" + # First call fails, second succeeds after reconnection + mock_session_group.call_tool.side_effect = [ + anyio.ClosedResourceError(), + mock_tool_result + ] + + with patch('agent_core.nodes.mcp_proxy_node.reconnect_mcp_server', new_callable=AsyncMock) as mock_reconnect: + mock_reconnect.return_value = True + + prep_res = { + "tool_params": {"arg1": "value1"}, + "shared_context": { + "runtime_objects": {"mcp_session_group": mock_session_group} + } + } + + result = await mcp_proxy_node.exec_async(prep_res) + + # Reconnection should have been attempted + mock_reconnect.assert_called_once_with(mock_session_group, "TestServer") + + # After reconnection, tool call should succeed + assert result["status"] == "success" + assert mock_session_group.call_tool.call_count == 2 + + @pytest.mark.asyncio + async def test_reconnect_failure_returns_error(self, mcp_proxy_node, mock_session_group): + """Test that failed reconnection returns appropriate error.""" + mock_session_group.call_tool.side_effect = anyio.ClosedResourceError() + + with patch('agent_core.nodes.mcp_proxy_node.reconnect_mcp_server', new_callable=AsyncMock) as mock_reconnect: + mock_reconnect.return_value = False # Reconnection fails + + prep_res = { + "tool_params": {"arg1": "value1"}, + "shared_context": { + "runtime_objects": {"mcp_session_group": mock_session_group} + } + } + + result = await mcp_proxy_node.exec_async(prep_res) + + assert result["status"] == "error" + assert "CRITICAL_CONNECTION_FAILURE" in result["payload"]["type"] + assert "Reconnection attempt" in result["error_message"] + + @pytest.mark.asyncio + async def test_max_reconnect_attempts_respected(self, mcp_proxy_node, mock_session_group): + """Test that reconnection stops after MAX_RECONNECT_ATTEMPTS.""" + # Always fail with ClosedResourceError + mock_session_group.call_tool.side_effect = anyio.ClosedResourceError() + + with patch('agent_core.nodes.mcp_proxy_node.reconnect_mcp_server', new_callable=AsyncMock) as mock_reconnect: + # Reconnection always "succeeds" but tool calls keep failing + mock_reconnect.return_value = True + + prep_res = { + "tool_params": {"arg1": "value1"}, + "shared_context": { + "runtime_objects": {"mcp_session_group": mock_session_group} + } + } + + result = await mcp_proxy_node.exec_async(prep_res) + + # Should have tried MAX_RECONNECT_ATTEMPTS - 1 reconnections + # (first attempt doesn't need reconnection) + from agent_core.nodes.mcp_proxy_node import MAX_RECONNECT_ATTEMPTS + assert mock_reconnect.call_count == MAX_RECONNECT_ATTEMPTS - 1 + assert result["status"] == "error" + + +class TestReconnectMCPServer: + """Tests for the reconnect_mcp_server function.""" + + @pytest.fixture + def mock_session_group(self): + """Create a mock session group with a dead session.""" + session_group = MagicMock() + + # Create a dead session + dead_session = MagicMock() + dead_session.server_name_from_config = "TestServer" + session_group.sessions = [dead_session] + + session_group.connect_to_server = AsyncMock() + session_group.disconnect_from_server = AsyncMock() + + return session_group + + @pytest.mark.asyncio + async def test_reconnect_success(self, mock_session_group): + """Test successful reconnection to an MCP server.""" + from agent_core.services.server_manager import reconnect_mcp_server + + # Create a mock new session + new_session = MagicMock() + mock_session_group.connect_to_server.return_value = new_session + + with patch('agent_core.services.server_manager.get_native_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = { + "TestServer": { + "transport": "stdio", + "command": "python", + "args": ["-m", "test_server"] + } + } + + result = await reconnect_mcp_server(mock_session_group, "TestServer") + + assert result is True + mock_session_group.disconnect_from_server.assert_called_once() + mock_session_group.connect_to_server.assert_called_once() + + @pytest.mark.asyncio + async def test_reconnect_server_not_found(self, mock_session_group): + """Test reconnection fails when server config not found.""" + from agent_core.services.server_manager import reconnect_mcp_server + + with patch('agent_core.services.server_manager.get_native_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = {} # No servers configured + + result = await reconnect_mcp_server(mock_session_group, "NonExistentServer") + + assert result is False + + @pytest.mark.asyncio + async def test_reconnect_unsupported_transport(self, mock_session_group): + """Test reconnection fails for unsupported transport types.""" + from agent_core.services.server_manager import reconnect_mcp_server + + with patch('agent_core.services.server_manager.get_native_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = { + "TestServer": { + "transport": "unsupported_type" + } + } + + result = await reconnect_mcp_server(mock_session_group, "TestServer") + + assert result is False + + @pytest.mark.asyncio + async def test_reconnect_http_transport(self, mock_session_group): + """Test reconnection works for HTTP transport.""" + from agent_core.services.server_manager import reconnect_mcp_server + + new_session = MagicMock() + mock_session_group.connect_to_server.return_value = new_session + + with patch('agent_core.services.server_manager.get_native_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = { + "TestServer": { + "transport": "http", + "url": "http://localhost:8080" + } + } + + result = await reconnect_mcp_server(mock_session_group, "TestServer") + + assert result is True + mock_session_group.connect_to_server.assert_called_once() + + @pytest.mark.asyncio + async def test_reconnect_handles_connection_error(self, mock_session_group): + """Test reconnection handles connection errors gracefully.""" + from agent_core.services.server_manager import reconnect_mcp_server + + mock_session_group.connect_to_server.side_effect = ConnectionRefusedError("Server not available") + + with patch('agent_core.services.server_manager.get_native_mcp_servers') as mock_get_servers: + mock_get_servers.return_value = { + "TestServer": { + "transport": "http", + "url": "http://localhost:8080" + } + } + + result = await reconnect_mcp_server(mock_session_group, "TestServer") + + assert result is False + + +class TestMCPProxyNodeMissingSessionGroup: + """Test MCPProxyNode behavior when session group is missing.""" + + @pytest.mark.asyncio + async def test_missing_session_group_returns_error(self): + """Test that missing session group returns appropriate error.""" + from agent_core.nodes.mcp_proxy_node import MCPProxyNode + + tool_info = { + "name": "test_tool", + "description": "A test tool", + "inputSchema": {"type": "object", "properties": {}} + } + + node = MCPProxyNode( + unique_tool_name="TestServer_test_tool", + original_tool_name="test_tool", + server_name="TestServer", + tool_info=tool_info + ) + + prep_res = { + "tool_params": {"arg1": "value1"}, + "shared_context": { + "runtime_objects": {} # No session group + } + } + + result = await node.exec_async(prep_res) + + assert result["status"] == "error" + assert "MCP Session Group not found" in result["error_message"] diff --git a/core/tests/test_message_utils.py b/core/tests/test_message_utils.py new file mode 100644 index 0000000..23d2a86 --- /dev/null +++ b/core/tests/test_message_utils.py @@ -0,0 +1,393 @@ +""" +Unit tests for agent_core.utils.message_utils module. + +This module tests the tool_call_safenet function that ensures message +history integrity for LLM calls, correcting proximity and symmetry +violations between assistant tool_calls and tool responses. + +Key concepts tested: +- Proximity correction: Messages shouldn't appear between tool calls and responses +- Symmetry correction: Every tool_call must have exactly one tool response +- Message reordering: Interloper messages are moved after tool responses +- Error injection: Missing tool responses get error placeholders +""" + +import pytest +from agent_core.utils.message_utils import tool_call_safenet + + +class TestEmptyAndNullInputs: + """Tests for edge cases with empty or null inputs.""" + + def test_empty_list_returns_empty(self): + """Test that empty message list returns empty list.""" + result = tool_call_safenet([], "test-agent") + assert result == [] + + def test_none_returns_empty(self): + """Test that None returns empty list.""" + # Function should handle None gracefully + result = tool_call_safenet(None, "test-agent") + assert result == [] + + +class TestPassthroughBehavior: + """Tests for messages that should pass through unchanged.""" + + def test_single_user_message(self): + """Test single user message passes through.""" + messages = [{"role": "user", "content": "Hello"}] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 1 + assert result[0] == messages[0] + + def test_user_assistant_conversation(self): + """Test basic user/assistant conversation passes through.""" + messages = [ + {"role": "user", "content": "Hi"}, + {"role": "assistant", "content": "Hello! How can I help?"}, + {"role": "user", "content": "Tell me a joke"}, + {"role": "assistant", "content": "Why did the chicken cross the road?"}, + ] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 4 + assert result == messages + + def test_assistant_without_tool_calls(self): + """Test assistant messages without tool_calls pass through.""" + messages = [ + {"role": "user", "content": "What is 2+2?"}, + {"role": "assistant", "content": "2+2 equals 4."}, + ] + result = tool_call_safenet(messages, "test-agent") + assert result == messages + + def test_assistant_with_empty_tool_calls(self): + """Test assistant with empty tool_calls list.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi", "tool_calls": []}, + ] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 2 + + +class TestValidToolCallSequences: + """Tests for valid tool call sequences that should remain unchanged.""" + + def test_single_tool_call_with_response(self): + """Test valid single tool call followed by response.""" + messages = [ + {"role": "user", "content": "Search for Python"}, + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "search", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "Found: Python docs"}, + ] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 3 + assert result[0]["role"] == "user" + assert result[1]["role"] == "assistant" + assert result[2]["role"] == "tool" + + def test_multiple_parallel_tool_calls(self): + """Test valid multiple parallel tool calls with all responses.""" + messages = [ + {"role": "user", "content": "Search for both"}, + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "search_a", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "search_b", "arguments": "{}"}}, + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "Result A"}, + {"role": "tool", "tool_call_id": "call-2", "content": "Result B"}, + ] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 4 + # Order should be preserved + assert result[2]["tool_call_id"] == "call-1" + assert result[3]["tool_call_id"] == "call-2" + + def test_sequential_tool_call_blocks(self): + """Test multiple sequential tool call/response blocks.""" + messages = [ + {"role": "user", "content": "Do two things"}, + # First tool call block + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "first_tool", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "First result"}, + # Second tool call block + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-2", "function": {"name": "second_tool", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": "call-2", "content": "Second result"}, + ] + result = tool_call_safenet(messages, "test-agent") + assert len(result) == 5 + assert result[1]["tool_calls"][0]["id"] == "call-1" + assert result[2]["tool_call_id"] == "call-1" + assert result[3]["tool_calls"][0]["id"] == "call-2" + assert result[4]["tool_call_id"] == "call-2" + + +class TestProximityViolations: + """Tests for proximity violation detection and correction.""" + + def test_user_message_between_call_and_response(self): + """Test user message inserted between tool call and response is moved.""" + messages = [ + {"role": "user", "content": "Search"}, + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "search", "arguments": "{}"}} + ]}, + {"role": "user", "content": "Interloper message"}, # Proximity violation + {"role": "tool", "tool_call_id": "call-1", "content": "Result"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Tool response should come immediately after assistant + assert result[2]["role"] == "tool" + assert result[2]["tool_call_id"] == "call-1" + # Interloper should be moved after tool response + assert result[3]["role"] == "user" + # Interloper content should have error message prepended + assert "[SAFENET ERROR]" in result[3]["content"] + + def test_multiple_interlopers(self): + """Test multiple interloper messages are all moved.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool", "arguments": "{}"}} + ]}, + {"role": "user", "content": "First interloper"}, + {"role": "user", "content": "Second interloper"}, + {"role": "tool", "tool_call_id": "call-1", "content": "Result"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Tool should be second (after assistant) + assert result[1]["role"] == "tool" + # Both interlopers should be after tool + assert result[2]["role"] == "user" + assert result[3]["role"] == "user" + + +class TestSymmetryViolations: + """Tests for symmetry violation detection and correction.""" + + def test_missing_tool_response(self): + """Test missing tool response gets error placeholder injected.""" + messages = [ + {"role": "user", "content": "Search"}, + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "search", "arguments": "{}"}} + ]}, + # Missing tool response for call-1 + {"role": "assistant", "content": "Moving on without result"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Should have injected an error response + assert len(result) >= 3 + # Find the injected tool response + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 1 + assert tool_responses[0]["tool_call_id"] == "call-1" + assert "no_response_from_tool" in tool_responses[0]["content"] + + def test_missing_one_of_multiple_responses(self): + """Test partial missing responses in parallel tool calls.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool_a", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "tool_b", "arguments": "{}"}}, + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "Result A"}, + # Missing response for call-2 + ] + result = tool_call_safenet(messages, "test-agent") + + # Should have both tool responses + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 2 + + # Find the injected error response + ids = [tr["tool_call_id"] for tr in tool_responses] + assert "call-1" in ids + assert "call-2" in ids + + # call-2 should have error content + call_2_response = next(tr for tr in tool_responses if tr["tool_call_id"] == "call-2") + assert "no_response_from_tool" in call_2_response["content"] + + def test_extra_tool_response(self): + """Test extra tool response (no matching call) is neutralized.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "Valid result"}, + {"role": "tool", "tool_call_id": "call-orphan", "content": "Orphan result"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Orphan should be neutralized (changed to assistant role) + orphan = next((m for m in result if "Orphan result" in str(m.get("content", ""))), None) + assert orphan is not None + assert orphan["role"] == "assistant" + assert "[SAFENET ERROR]" in orphan["content"] + assert "tool_call_id" not in orphan + + def test_all_responses_missing(self): + """Test all tool responses missing for multiple calls.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool_a", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "tool_b", "arguments": "{}"}}, + {"id": "call-3", "function": {"name": "tool_c", "arguments": "{}"}}, + ]}, + # No tool responses at all + ] + result = tool_call_safenet(messages, "test-agent") + + # Should inject 3 error responses + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 3 + + +class TestComplexScenarios: + """Tests for complex real-world scenarios.""" + + def test_mixed_violations(self): + """Test both proximity and symmetry violations together.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool_a", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "tool_b", "arguments": "{}"}}, + ]}, + {"role": "user", "content": "Interloper"}, # Proximity violation + {"role": "tool", "tool_call_id": "call-1", "content": "Result A"}, + # call-2 missing - Symmetry violation + ] + result = tool_call_safenet(messages, "test-agent") + + # Should fix both issues + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 2 # Both calls have responses + + # Interloper should be after tool responses + user_msgs = [i for i, m in enumerate(result) if m.get("role") == "user"] + tool_indices = [i for i, m in enumerate(result) if m.get("role") == "tool"] + for user_idx in user_msgs: + for tool_idx in tool_indices: + assert user_idx > tool_idx or result[user_idx] != messages[1] + + def test_long_conversation_with_issues(self): + """Test safenet in a longer conversation with multiple issues.""" + messages = [ + {"role": "user", "content": "Start task"}, + {"role": "assistant", "content": "I'll help"}, + # First valid tool call block + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "init", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": "call-1", "content": "Initialized"}, + # Second block with proximity violation + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-2", "function": {"name": "process", "arguments": "{}"}} + ]}, + {"role": "user", "content": "Hurry up!"}, # Interloper + {"role": "tool", "tool_call_id": "call-2", "content": "Processed"}, + # Third block with missing response + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-3", "function": {"name": "finalize", "arguments": "{}"}} + ]}, + {"role": "assistant", "content": "Done!"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Should have all tool responses + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 3 # All three calls have responses + + def test_preserves_original_content(self): + """Test that non-violated content is preserved exactly.""" + original = {"role": "user", "content": "Exact content here"} + messages = [original] + result = tool_call_safenet(messages, "test-agent") + + # Should be the exact same object + assert result[0] is original + + +class TestToolCallMetadata: + """Tests for preserving tool call metadata.""" + + def test_preserves_tool_name_in_error(self): + """Test that injected errors include original tool name.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "specific_tool_name", "arguments": "{}"}} + ]}, + ] + result = tool_call_safenet(messages, "test-agent") + + tool_response = next(m for m in result if m.get("role") == "tool") + assert tool_response.get("name") == "specific_tool_name" + + def test_handles_tool_call_without_function_name(self): + """Test graceful handling when tool call lacks function name.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {}} # Missing name + ]}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Should still inject error response + tool_responses = [m for m in result if m.get("role") == "tool"] + assert len(tool_responses) == 1 + + +class TestNullToolCallIds: + """Tests for handling None tool_call_ids.""" + + def test_tool_response_with_none_id(self): + """Test that tool responses with None id are handled correctly.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool", "arguments": "{}"}} + ]}, + {"role": "tool", "tool_call_id": None, "content": "Bad response"}, # None ID + {"role": "tool", "tool_call_id": "call-1", "content": "Good response"}, + ] + result = tool_call_safenet(messages, "test-agent") + + # Should not crash and should handle the None gracefully + assert len(result) >= 2 + + +class TestImmutability: + """Tests to verify the function doesn't mutate input.""" + + def test_does_not_mutate_input_messages(self): + """Test that original messages list is not mutated.""" + messages = [ + {"role": "assistant", "content": None, "tool_calls": [ + {"id": "call-1", "function": {"name": "tool", "arguments": "{}"}} + ]}, + {"role": "user", "content": "Interloper"}, + {"role": "tool", "tool_call_id": "call-1", "content": "Result"}, + ] + + # Create deep copy to compare + import copy + original = copy.deepcopy(messages) + + result = tool_call_safenet(messages, "test-agent") + + # Verify structure wasn't mutated (note: content might be modified in copies) + assert len(messages) == len(original) + assert messages[0]["role"] == original[0]["role"] + assert messages[1]["role"] == original[1]["role"] + assert messages[2]["role"] == original[2]["role"] diff --git a/core/tests/test_models.py b/core/tests/test_models.py new file mode 100644 index 0000000..125f01a --- /dev/null +++ b/core/tests/test_models.py @@ -0,0 +1,496 @@ +""" +Unit tests for agent_core.models.context and agent_core.models.turn modules. + +These modules define TypedDict structures for the context system: +- RunContext: The root context for a business run +- SubContext: Context for individual sub-processes (Partner, Principal, Associate) +- TeamState: Shared state across team members +- Turn: Event log structure for turn tracking + +Tests focus on: +- Type structure validation +- Required vs optional fields +- Typical usage patterns +""" + +import pytest +from typing import get_type_hints, get_origin, get_args +from agent_core.models.context import ( + RunContext, + RunContextMeta, + RunContextConfig, + RunContextRuntime, + RunContextSubContexts, + SubContext, + SubContextMeta, + SubContextState, + SubContextRuntimeObjects, + SubContextRefs, + TeamState, +) +from agent_core.models.turn import ( + Turn, + TurnInputs, + AgentInfo, + LLMInteraction, + LLMAttempt, + ToolInteraction, + ProcessedInboxItemLog, +) + + +class TestTeamState: + """Tests for TeamState TypedDict.""" + + def test_can_create_empty_team_state(self): + """Test TeamState can be created with no fields (total=False).""" + state: TeamState = {} + assert isinstance(state, dict) + + def test_typical_team_state(self): + """Test creating a typical TeamState instance.""" + state: TeamState = { + "question": "What is machine learning?", + "work_modules": {"module-1": {"status": "complete"}}, + "_work_module_next_id": 2, + "profiles_list_instance_ids": ["partner-1", "principal-1"], + "is_principal_flow_running": True, + "dispatch_history": [], + "turns": [], + "partner_directives_queue": [], + } + assert state["question"] == "What is machine learning?" + assert state["is_principal_flow_running"] is True + + def test_work_modules_field(self): + """Test work_modules contains arbitrary nested structure.""" + state: TeamState = { + "work_modules": { + "WM001": { + "id": "WM001", + "title": "Analysis Module", + "status": "in_progress", + "deliverables": {}, + } + }, + "_work_module_next_id": 2, + } + assert "WM001" in state["work_modules"] + assert state["work_modules"]["WM001"]["status"] == "in_progress" + + +class TestSubContextMeta: + """Tests for SubContextMeta TypedDict.""" + + def test_required_fields(self): + """Test SubContextMeta has required fields.""" + meta: SubContextMeta = { + "run_id": "run-123", + "agent_id": "agent-abc", + "parent_agent_id": None, + "assigned_role_name": "Analyst", + } + assert meta["run_id"] == "run-123" + assert meta["agent_id"] == "agent-abc" + assert meta["parent_agent_id"] is None + assert meta["assigned_role_name"] == "Analyst" + + def test_all_fields_present(self): + """Test all SubContextMeta fields can be set.""" + hints = get_type_hints(SubContextMeta) + expected_fields = {"run_id", "agent_id", "parent_agent_id", "assigned_role_name"} + assert set(hints.keys()) == expected_fields + + +class TestSubContextState: + """Tests for SubContextState TypedDict.""" + + def test_can_create_minimal_state(self): + """Test SubContextState can be created with minimal fields.""" + state: SubContextState = { + "messages": [], + } + assert state["messages"] == [] + + def test_full_state_structure(self): + """Test creating a comprehensive SubContextState.""" + state: SubContextState = { + "messages": [{"role": "user", "content": "Hello"}], + "current_action": None, + "inbox": [], + "flags": {"handover_requested": False}, + "initial_parameters": {"task": "analyze"}, + "deliverables": {"summary": "Complete"}, + "last_activity_timestamp": "2024-01-01T00:00:00Z", + "current_iteration_count": 5, + "archived_messages_history": [], + "status_summary_for_partner": {}, + "execution_milestones": [], + "tool_inputs": {}, + "current_tool_call_id": None, + "current_actor_id": "actor-1", + "last_turn_id": "turn-999", + "consecutive_empty_llm_responses": 0, + "_current_llm_stream_id": None, + "agent_start_utc_timestamp": "2024-01-01T00:00:00Z", + "profiles_list_instance_ids": [], + "principal_launch_config_history": [], + } + assert len(state["messages"]) == 1 + assert state["current_iteration_count"] == 5 + + +class TestSubContext: + """Tests for SubContext TypedDict.""" + + def test_subcontext_structure(self): + """Test SubContext combines meta, state, runtime_objects, and refs.""" + # Create minimal valid SubContext + sub_ctx: SubContext = { + "meta": { + "run_id": "run-1", + "agent_id": "agent-1", + "parent_agent_id": None, + "assigned_role_name": None, + }, + "state": {"messages": []}, + "runtime_objects": {}, + "refs": { + "run": {}, # Simplified + "team": {}, + }, + } + assert sub_ctx["meta"]["agent_id"] == "agent-1" + assert isinstance(sub_ctx["state"], dict) + + +class TestRunContextMeta: + """Tests for RunContextMeta TypedDict.""" + + def test_status_literals(self): + """Test RunContextMeta status field accepts valid literals.""" + valid_statuses = ["CREATED", "RUNNING", "AWAITING_INPUT", "COMPLETED", "FAILED", "CANCELLED"] + + for status in valid_statuses: + meta: RunContextMeta = { + "run_id": "run-test", + "run_type": "research", + "creation_timestamp": "2024-01-01T00:00:00Z", + "status": status, + } + assert meta["status"] == status + + +class TestRunContext: + """Tests for RunContext TypedDict (the root context).""" + + def test_run_context_structure(self): + """Test RunContext has all required components.""" + hints = get_type_hints(RunContext) + expected_fields = {"meta", "config", "team_state", "runtime", "sub_context_refs", "project_id"} + assert set(hints.keys()) == expected_fields + + def test_minimal_run_context(self): + """Test creating a minimal RunContext.""" + run_ctx: RunContext = { + "meta": { + "run_id": "run-main", + "run_type": "chat", + "creation_timestamp": "2024-01-01T00:00:00Z", + "status": "RUNNING", + }, + "config": { + "agent_profiles_store": {}, + "shared_llm_configs_ref": {}, + }, + "team_state": {}, + "runtime": {}, + "sub_context_refs": { + "_partner_context_ref": None, + "_principal_context_ref": None, + "_ongoing_associate_tasks": {}, + }, + "project_id": "project-abc", + } + assert run_ctx["meta"]["status"] == "RUNNING" + assert run_ctx["project_id"] == "project-abc" + + +class TestAgentInfo: + """Tests for AgentInfo TypedDict.""" + + def test_agent_info_fields(self): + """Test AgentInfo has expected fields.""" + info: AgentInfo = { + "agent_id": "principal-agent-1", + "profile_logical_name": "Base_Principal", + "profile_instance_id": "instance-123", + "assigned_role_name": "Lead Analyst", + } + assert info["agent_id"] == "principal-agent-1" + assert info["assigned_role_name"] == "Lead Analyst" + + +class TestLLMAttempt: + """Tests for LLMAttempt TypedDict.""" + + def test_successful_attempt(self): + """Test LLMAttempt for successful call.""" + attempt: LLMAttempt = { + "stream_id": "stream-abc", + "status": "success", + "error": None, + } + assert attempt["status"] == "success" + + def test_failed_attempt(self): + """Test LLMAttempt for failed call.""" + attempt: LLMAttempt = { + "stream_id": "stream-xyz", + "status": "failed", + "error": "Connection timeout", + } + assert attempt["status"] == "failed" + assert attempt["error"] == "Connection timeout" + + +class TestLLMInteraction: + """Tests for LLMInteraction TypedDict.""" + + def test_llm_interaction_with_usage(self): + """Test LLMInteraction with usage tracking fields.""" + interaction: LLMInteraction = { + "status": "completed", + "attempts": [ + {"stream_id": "s1", "status": "success", "error": None} + ], + "final_request": {"messages": []}, + "final_response": {"content": "Response text"}, + "predicted_usage": {"input_tokens": 100, "output_tokens": 50}, + "actual_usage": {"input_tokens": 105, "output_tokens": 48}, + } + assert interaction["predicted_usage"]["input_tokens"] == 100 + assert interaction["actual_usage"]["output_tokens"] == 48 + + +class TestToolInteraction: + """Tests for ToolInteraction TypedDict.""" + + def test_completed_tool_interaction(self): + """Test ToolInteraction for completed tool call.""" + interaction: ToolInteraction = { + "tool_call_id": "call-123", + "tool_name": "search_documents", + "start_time": "2024-01-01T10:00:00Z", + "end_time": "2024-01-01T10:00:05Z", + "status": "completed", + "input_params": {"query": "test"}, + "result_payload": {"results": ["doc1", "doc2"]}, + "error_details": None, + } + assert interaction["status"] == "completed" + assert interaction["tool_name"] == "search_documents" + + def test_error_tool_interaction(self): + """Test ToolInteraction for failed tool call.""" + interaction: ToolInteraction = { + "tool_call_id": "call-456", + "tool_name": "external_api", + "start_time": "2024-01-01T11:00:00Z", + "end_time": "2024-01-01T11:00:10Z", + "status": "error", + "input_params": {"endpoint": "/data"}, + "result_payload": None, + "error_details": "API returned 500 Internal Server Error", + } + assert interaction["status"] == "error" + assert "500" in interaction["error_details"] + + +class TestProcessedInboxItemLog: + """Tests for ProcessedInboxItemLog TypedDict.""" + + def test_full_inbox_log(self): + """Test ProcessedInboxItemLog with all fields.""" + log: ProcessedInboxItemLog = { + "item_id": "inbox-item-1", + "source": "partner_directive", + "triggering_observer_id": "observer-abc", + "handling_strategy_source": "profile", + "ingestor_used": "directive_ingestor", + "injection_mode": "append", + "injected_content": "New directive: analyze data", + "predicted_token_count": 150, + } + assert log["handling_strategy_source"] == "profile" + assert log["predicted_token_count"] == 150 + + +class TestTurnInputs: + """Tests for TurnInputs TypedDict.""" + + def test_turn_inputs_with_processed_items(self): + """Test TurnInputs containing processed inbox items.""" + inputs: TurnInputs = { + "processed_inbox_items": [ + { + "item_id": "item-1", + "source": "user_input", + "ingestor_used": "user_message_ingestor", + "injection_mode": "prepend", + "injected_content": "User asked about AI", + } + ] + } + assert len(inputs["processed_inbox_items"]) == 1 + + +class TestTurn: + """Tests for Turn TypedDict (the main turn tracking structure).""" + + def test_turn_structure(self): + """Test Turn has all expected fields.""" + hints = get_type_hints(Turn) + expected_fields = { + "turn_id", "run_id", "flow_id", "agent_info", "turn_type", + "status", "start_time", "end_time", "source_turn_ids", + "source_tool_call_id", "inputs", "outputs", "llm_interaction", + "tool_interactions", "metadata", "error_details" + } + assert set(hints.keys()) == expected_fields + + def test_minimal_turn(self): + """Test creating a minimal Turn.""" + turn: Turn = { + "turn_id": "turn-001", + "run_id": "run-main", + "flow_id": "flow-principal", + "agent_info": { + "agent_id": "agent-1", + "profile_logical_name": "Base_Principal", + "profile_instance_id": "inst-1", + "assigned_role_name": None, + }, + "turn_type": "agent_turn", + "status": "completed", + "start_time": "2024-01-01T10:00:00Z", + "end_time": "2024-01-01T10:00:30Z", + "source_turn_ids": [], + "source_tool_call_id": None, + "inputs": {"processed_inbox_items": []}, + "outputs": {}, + "llm_interaction": None, + "tool_interactions": [], + "metadata": None, + "error_details": None, + } + assert turn["turn_id"] == "turn-001" + assert turn["turn_type"] == "agent_turn" + assert turn["status"] == "completed" + + def test_turn_type_literals(self): + """Test Turn turn_type accepts valid literals.""" + valid_types = ["agent_turn", "dispatch_turn", "aggregation_turn", "user_turn"] + + for turn_type in valid_types: + turn: Turn = { + "turn_id": "t1", + "run_id": "r1", + "flow_id": "f1", + "agent_info": { + "agent_id": "a1", + "profile_logical_name": "Test", + "profile_instance_id": "i1", + "assigned_role_name": None, + }, + "turn_type": turn_type, + "status": "running", + "start_time": "2024-01-01T00:00:00Z", + "end_time": None, + "source_turn_ids": [], + "source_tool_call_id": None, + "inputs": {}, + "outputs": {}, + "llm_interaction": None, + "tool_interactions": [], + "metadata": None, + "error_details": None, + } + assert turn["turn_type"] == turn_type + + def test_turn_with_llm_and_tools(self): + """Test Turn with full LLM interaction and tool calls.""" + turn: Turn = { + "turn_id": "turn-full", + "run_id": "run-1", + "flow_id": "flow-1", + "agent_info": { + "agent_id": "agent-full", + "profile_logical_name": "TestAgent", + "profile_instance_id": "inst-full", + "assigned_role_name": "Analyst", + }, + "turn_type": "agent_turn", + "status": "completed", + "start_time": "2024-01-01T12:00:00Z", + "end_time": "2024-01-01T12:01:00Z", + "source_turn_ids": ["turn-prev"], + "source_tool_call_id": "call-trigger", + "inputs": { + "processed_inbox_items": [ + { + "item_id": "inbox-1", + "source": "directive", + "ingestor_used": "default", + "injection_mode": "append", + "injected_content": "Process this", + } + ] + }, + "outputs": {"state_keys_modified": ["deliverables"]}, + "llm_interaction": { + "status": "completed", + "attempts": [{"stream_id": "s1", "status": "success", "error": None}], + "final_request": "[omitted]", + "final_response": {"content": "Done"}, + "predicted_usage": {"input_tokens": 500, "output_tokens": 100}, + "actual_usage": {"input_tokens": 510, "output_tokens": 95}, + }, + "tool_interactions": [ + { + "tool_call_id": "tc-1", + "tool_name": "search", + "start_time": "2024-01-01T12:00:10Z", + "end_time": "2024-01-01T12:00:15Z", + "status": "completed", + "input_params": {"query": "test"}, + "result_payload": {"results": []}, + "error_details": None, + } + ], + "metadata": {"iteration": 3}, + "error_details": None, + } + + assert len(turn["tool_interactions"]) == 1 + assert turn["llm_interaction"]["status"] == "completed" + assert turn["outputs"]["state_keys_modified"] == ["deliverables"] + + +class TestTypeAnnotations: + """Tests verifying TypedDict type annotations are correct.""" + + def test_run_context_meta_types(self): + """Test RunContextMeta field types.""" + hints = get_type_hints(RunContextMeta) + assert hints["run_id"] == str + assert hints["run_type"] == str + assert hints["creation_timestamp"] == str + + def test_team_state_has_optional_fields(self): + """Test TeamState uses total=False (all fields optional).""" + # We can verify this by checking __required_keys__ and __optional_keys__ + if hasattr(TeamState, '__required_keys__'): + # Python 3.9+ TypedDict + assert len(TeamState.__required_keys__) == 0 + assert len(TeamState.__optional_keys__) > 0 diff --git a/core/tests/test_profile_loading.py b/core/tests/test_profile_loading.py new file mode 100644 index 0000000..bda21da --- /dev/null +++ b/core/tests/test_profile_loading.py @@ -0,0 +1,212 @@ +""" +Tests for profile loading and toolset access verification. + +These tests verify that the profile changes we made (adding flow_control_summary +to Base_Associate) are properly inherited and that profile loading works correctly. +""" +import pytest +import sys +from pathlib import Path + +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + +from agent_profiles.loader import ( + AGENT_PROFILES, + get_global_active_profile_by_logical_name_copy, + _deep_merge, +) + + +class TestProfileInheritance: + """Tests for profile inheritance mechanism.""" + + def test_deep_merge_simple_dict(self): + """Simple dict merge should work.""" + parent = {"a": 1, "b": 2} + child = {"b": 3, "c": 4} + result = _deep_merge(parent, child) + + assert result == {"a": 1, "b": 3, "c": 4} + + def test_deep_merge_nested_dict(self): + """Nested dict merge should be recursive.""" + parent = {"outer": {"inner1": 1, "inner2": 2}} + child = {"outer": {"inner2": 3, "inner3": 4}} + result = _deep_merge(parent, child) + + assert result["outer"]["inner1"] == 1 + assert result["outer"]["inner2"] == 3 + assert result["outer"]["inner3"] == 4 + + def test_deep_merge_list_with_ids(self): + """Lists of dicts with 'id' keys should merge by id.""" + parent = {"items": [{"id": "a", "val": 1}, {"id": "b", "val": 2}]} + child = {"items": [{"id": "b", "val": 3}, {"id": "c", "val": 4}]} + result = _deep_merge(parent, child) + + # Should have 3 items: a from parent, b from child (overwritten), c from child + assert len(result["items"]) == 3 + ids = {item["id"] for item in result["items"]} + assert ids == {"a", "b", "c"} + + # b should have child's value + b_item = next(item for item in result["items"] if item["id"] == "b") + assert b_item["val"] == 3 + + def test_deep_merge_simple_list(self): + """Simple lists should concatenate and dedupe.""" + parent = {"tags": ["a", "b"]} + child = {"tags": ["b", "c"]} + result = _deep_merge(parent, child) + + assert set(result["tags"]) == {"a", "b", "c"} + + +class TestBaseAssociateToolsets: + """Tests for Base_Associate toolset configuration.""" + + def test_base_associate_exists(self): + """Base_Associate profile should exist.""" + profile = get_global_active_profile_by_logical_name_copy("Base_Associate") + assert profile is not None + assert profile["name"] == "Base_Associate" + + def test_base_associate_has_flow_control_end(self): + """Base_Associate should have flow_control_end toolset.""" + profile = get_global_active_profile_by_logical_name_copy("Base_Associate") + assert profile is not None + + allowed = profile.get("tool_access_policy", {}).get("allowed_toolsets", []) + assert "flow_control_end" in allowed + + def test_base_associate_has_flow_control_summary(self): + """Base_Associate should have flow_control_summary toolset (our fix).""" + profile = get_global_active_profile_by_logical_name_copy("Base_Associate") + assert profile is not None + + allowed = profile.get("tool_access_policy", {}).get("allowed_toolsets", []) + assert "flow_control_summary" in allowed, \ + "Base_Associate should have flow_control_summary - this is the toolset fix we added" + + +class TestAssociateProfilesInheritToolsets: + """Tests that Associate profiles inherit toolsets from Base_Associate.""" + + @pytest.fixture + def associate_profiles(self): + """Get all profiles that inherit from Base_Associate.""" + associates = [] + for profile in AGENT_PROFILES.values(): + if profile.get("base_profile") == "Base_Associate" or \ + profile.get("type") == "associate": + associates.append(profile) + return associates + + def test_associates_have_base_toolsets(self, associate_profiles): + """All Associate profiles should have the base toolsets.""" + for profile in associate_profiles: + allowed = profile.get("tool_access_policy", {}).get("allowed_toolsets", []) + + # Should inherit flow_control_end + assert "flow_control_end" in allowed, \ + f"{profile['name']} missing flow_control_end" + + # Should inherit flow_control_summary (our fix) + assert "flow_control_summary" in allowed, \ + f"{profile['name']} missing flow_control_summary - inheritance may be broken" + + def test_localrag_has_summarization_access(self): + """Associate_LocalRAG should have access to generate_message_summary.""" + profile = get_global_active_profile_by_logical_name_copy("Associate_LocalRAG_EN") + + if profile is None: + pytest.skip("Associate_LocalRAG_EN profile not found") + + allowed = profile.get("tool_access_policy", {}).get("allowed_toolsets", []) + assert "flow_control_summary" in allowed, \ + "LocalRAG needs flow_control_summary to call generate_message_summary" + + def test_smartrag_has_summarization_access(self): + """Associate_SmartRAG should have access to generate_message_summary.""" + profile = get_global_active_profile_by_logical_name_copy("Associate_SmartRAG_EN") + + if profile is None: + pytest.skip("Associate_SmartRAG_EN profile not found") + + allowed = profile.get("tool_access_policy", {}).get("allowed_toolsets", []) + assert "flow_control_summary" in allowed, \ + "SmartRAG needs flow_control_summary to call generate_message_summary" + + +class TestProfileToolReferences: + """Tests that profiles don't reference non-existent tools.""" + + def test_localrag_references_correct_tool(self): + """LocalRAG should reference generate_message_summary, not handover_to_summary.""" + profile = get_global_active_profile_by_logical_name_copy("Associate_LocalRAG_EN") + + if profile is None: + pytest.skip("Associate_LocalRAG_EN profile not found") + + # Convert profile to string to search for tool references + profile_str = str(profile) + + # Should NOT reference the non-existent tool + assert "handover_to_summary" not in profile_str, \ + "LocalRAG still references non-existent handover_to_summary tool" + + # Should reference the correct tool + assert "generate_message_summary" in profile_str, \ + "LocalRAG should reference generate_message_summary" + + def test_smartrag_references_correct_tool(self): + """SmartRAG should reference generate_message_summary, not handover_to_summary.""" + profile = get_global_active_profile_by_logical_name_copy("Associate_SmartRAG_EN") + + if profile is None: + pytest.skip("Associate_SmartRAG_EN profile not found") + + profile_str = str(profile) + + assert "handover_to_summary" not in profile_str, \ + "SmartRAG still references non-existent handover_to_summary tool" + + assert "generate_message_summary" in profile_str, \ + "SmartRAG should reference generate_message_summary" + + +class TestProfileLoading: + """Tests for general profile loading functionality.""" + + def test_profiles_loaded(self): + """At least some profiles should be loaded.""" + assert len(AGENT_PROFILES) > 0 + + def test_base_agent_exists(self): + """Base_Agent profile should exist.""" + profile = get_global_active_profile_by_logical_name_copy("Base_Agent") + assert profile is not None + + def test_profiles_have_required_fields(self): + """All profiles should have required metadata fields.""" + for profile_id, profile in AGENT_PROFILES.items(): + assert "name" in profile, f"Profile {profile_id} missing name" + assert "profile_id" in profile, f"Profile {profile_id} missing profile_id" + assert "is_active" in profile, f"Profile {profile_id} missing is_active" + + def test_get_profile_returns_copy(self): + """get_global_active_profile_by_logical_name_copy should return a copy.""" + profile1 = get_global_active_profile_by_logical_name_copy("Base_Agent") + profile2 = get_global_active_profile_by_logical_name_copy("Base_Agent") + + if profile1 is None: + pytest.skip("Base_Agent not found") + + # Should be equal but not the same object + assert profile1 == profile2 + assert profile1 is not profile2 + + # Modifying one should not affect the other + profile1["test_mutation"] = True + assert "test_mutation" not in profile2 diff --git a/core/tests/test_profile_utils.py b/core/tests/test_profile_utils.py new file mode 100644 index 0000000..32c0037 --- /dev/null +++ b/core/tests/test_profile_utils.py @@ -0,0 +1,482 @@ +""" +Unit tests for agent_core.framework.profile_utils module. + +This module tests profile retrieval utilities that find agent profiles +from the profiles store based on name, instance ID, revision, and active status. + +Key functionality tested: +- get_active_profile_by_name: Find latest active profile by logical name +- get_profile_by_instance_id: Find profile by UUID instance ID +- get_active_profile_instance_id_by_name: Get instance ID by name +- Revision (rev) comparison for multiple active versions +- Timestamp comparison for same-revision profiles +- is_deleted filtering +""" + +import pytest +from datetime import datetime, timezone +from agent_core.framework.profile_utils import ( + get_active_profile_by_name, + get_profile_by_instance_id, + get_active_profile_instance_id_by_name, +) + + +class TestGetActiveProfileByName: + """Tests for get_active_profile_by_name function.""" + + def test_finds_single_active_profile(self): + """Test finding a single active profile by name.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + } + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + assert result is not None + assert result["name"] == "TestProfile" + assert result["profile_id"] == "uuid-1" + + def test_returns_none_for_missing_name(self): + """Test returns None when profile name not found.""" + profiles_store = { + "uuid-1": {"name": "OtherProfile", "is_active": True, "is_deleted": False} + } + + result = get_active_profile_by_name(profiles_store, "NonExistent") + + assert result is None + + def test_ignores_inactive_profiles(self): + """Test that inactive profiles are not returned.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "TestProfile", + "is_active": False, # Inactive + "is_deleted": False, + } + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + assert result is None + + def test_ignores_deleted_profiles(self): + """Test that deleted profiles are not returned.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "TestProfile", + "is_active": True, + "is_deleted": True, # Deleted + } + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + assert result is None + + def test_selects_highest_revision(self): + """Test that highest revision is selected when multiple active versions exist.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + }, + "uuid-2": { + "profile_id": "uuid-2", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 3, # Higher revision + }, + "uuid-3": { + "profile_id": "uuid-3", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 2, + }, + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + assert result["profile_id"] == "uuid-2" + assert result["rev"] == 3 + + def test_selects_newer_timestamp_for_same_revision(self): + """Test that newer timestamp is selected when revisions are equal.""" + profiles_store = { + "uuid-older": { + "profile_id": "uuid-older", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-01-01T10:00:00Z", + }, + "uuid-newer": { + "profile_id": "uuid-newer", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, # Same revision + "timestamp": "2024-01-02T10:00:00Z", # Newer + }, + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + assert result["profile_id"] == "uuid-newer" + + def test_handles_missing_timestamp(self): + """Test handling profiles with missing timestamps.""" + profiles_store = { + "uuid-with-ts": { + "profile_id": "uuid-with-ts", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-01-01T10:00:00Z", + }, + "uuid-no-ts": { + "profile_id": "uuid-no-ts", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, # Same revision, no timestamp + }, + } + + # Should not crash, should return one of them + result = get_active_profile_by_name(profiles_store, "TestProfile") + assert result is not None + assert result["name"] == "TestProfile" + + def test_returns_deep_copy(self): + """Test that returned profile is a deep copy.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "nested": {"key": "value"}, + } + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + # Modify the result + result["nested"]["key"] = "modified" + + # Original should be unchanged + assert profiles_store["uuid-1"]["nested"]["key"] == "value" + + def test_empty_profiles_store(self): + """Test with empty profiles store.""" + result = get_active_profile_by_name({}, "TestProfile") + assert result is None + + def test_none_profiles_store(self): + """Test with None profiles store.""" + result = get_active_profile_by_name(None, "TestProfile") + assert result is None + + def test_none_name(self): + """Test with None name.""" + profiles_store = {"uuid-1": {"name": "Test", "is_active": True, "is_deleted": False}} + result = get_active_profile_by_name(profiles_store, None) + assert result is None + + def test_empty_name(self): + """Test with empty string name.""" + profiles_store = {"uuid-1": {"name": "Test", "is_active": True, "is_deleted": False}} + result = get_active_profile_by_name(profiles_store, "") + assert result is None + + def test_missing_rev_defaults_to_zero(self): + """Test profiles without rev field default to 0.""" + profiles_store = { + "uuid-no-rev": { + "profile_id": "uuid-no-rev", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + # No 'rev' field + }, + "uuid-rev-1": { + "profile_id": "uuid-rev-1", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + }, + } + + result = get_active_profile_by_name(profiles_store, "TestProfile") + + # rev=1 should win over no rev (defaults to 0) + assert result["profile_id"] == "uuid-rev-1" + + +class TestGetProfileByInstanceId: + """Tests for get_profile_by_instance_id function.""" + + def test_finds_profile_by_id(self): + """Test finding profile by instance ID.""" + profiles_store = { + "uuid-abc": { + "profile_id": "uuid-abc", + "name": "TestProfile", + "is_deleted": False, + } + } + + result = get_profile_by_instance_id(profiles_store, "uuid-abc") + + assert result is not None + assert result["profile_id"] == "uuid-abc" + + def test_returns_none_for_missing_id(self): + """Test returns None for non-existent instance ID.""" + profiles_store = {"uuid-1": {"name": "Test"}} + + result = get_profile_by_instance_id(profiles_store, "nonexistent") + + assert result is None + + def test_returns_none_for_deleted_profile(self): + """Test returns None for deleted profile.""" + profiles_store = { + "uuid-deleted": { + "profile_id": "uuid-deleted", + "name": "DeletedProfile", + "is_deleted": True, + } + } + + result = get_profile_by_instance_id(profiles_store, "uuid-deleted") + + assert result is None + + def test_returns_deep_copy(self): + """Test that returned profile is a deep copy.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "Test", + "is_deleted": False, + "data": {"nested": "value"}, + } + } + + result = get_profile_by_instance_id(profiles_store, "uuid-1") + result["data"]["nested"] = "modified" + + # Original unchanged + assert profiles_store["uuid-1"]["data"]["nested"] == "value" + + def test_empty_profiles_store(self): + """Test with empty profiles store.""" + result = get_profile_by_instance_id({}, "uuid-1") + assert result is None + + def test_none_profiles_store(self): + """Test with None profiles store.""" + result = get_profile_by_instance_id(None, "uuid-1") + assert result is None + + def test_none_instance_id(self): + """Test with None instance ID.""" + profiles_store = {"uuid-1": {"name": "Test"}} + result = get_profile_by_instance_id(profiles_store, None) + assert result is None + + def test_empty_instance_id(self): + """Test with empty string instance ID.""" + profiles_store = {"uuid-1": {"name": "Test"}} + result = get_profile_by_instance_id(profiles_store, "") + assert result is None + + +class TestGetActiveProfileInstanceIdByName: + """Tests for get_active_profile_instance_id_by_name function.""" + + def test_returns_instance_id(self): + """Test returns instance ID for active profile.""" + profiles_store = { + "uuid-target": { + "profile_id": "uuid-target", + "name": "TargetProfile", + "is_active": True, + "is_deleted": False, + } + } + + result = get_active_profile_instance_id_by_name(profiles_store, "TargetProfile") + + assert result == "uuid-target" + + def test_returns_none_when_not_found(self): + """Test returns None when profile not found.""" + profiles_store = {} + + result = get_active_profile_instance_id_by_name(profiles_store, "NonExistent") + + assert result is None + + def test_returns_highest_rev_instance_id(self): + """Test returns instance ID of highest revision.""" + profiles_store = { + "uuid-rev1": { + "profile_id": "uuid-rev1", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 1, + }, + "uuid-rev2": { + "profile_id": "uuid-rev2", + "name": "TestProfile", + "is_active": True, + "is_deleted": False, + "rev": 2, + }, + } + + result = get_active_profile_instance_id_by_name(profiles_store, "TestProfile") + + assert result == "uuid-rev2" + + +class TestTimestampParsing: + """Tests for timestamp parsing edge cases.""" + + def test_handles_iso_format_with_z(self): + """Test timestamp parsing with Z timezone.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "Test", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-06-15T14:30:00Z", + }, + "uuid-2": { + "profile_id": "uuid-2", + "name": "Test", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-06-15T14:30:01Z", # 1 second newer + }, + } + + result = get_active_profile_by_name(profiles_store, "Test") + assert result["profile_id"] == "uuid-2" + + def test_handles_iso_format_with_offset(self): + """Test timestamp parsing with timezone offset.""" + profiles_store = { + "uuid-1": { + "profile_id": "uuid-1", + "name": "Test", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-06-15T14:30:00+00:00", + }, + } + + result = get_active_profile_by_name(profiles_store, "Test") + assert result is not None + + def test_handles_invalid_timestamp_gracefully(self): + """Test that invalid timestamps don't crash.""" + profiles_store = { + "uuid-bad-ts": { + "profile_id": "uuid-bad-ts", + "name": "Test", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "not-a-valid-timestamp", + }, + "uuid-good-ts": { + "profile_id": "uuid-good-ts", + "name": "Test", + "is_active": True, + "is_deleted": False, + "rev": 1, + "timestamp": "2024-01-01T00:00:00Z", + }, + } + + # Should not crash and should return one of them + result = get_active_profile_by_name(profiles_store, "Test") + assert result is not None + + +class TestMultipleProfiles: + """Tests with multiple profiles in store.""" + + def test_finds_correct_profile_among_many(self): + """Test finding correct profile in a store with many profiles.""" + profiles_store = { + f"uuid-{i}": { + "profile_id": f"uuid-{i}", + "name": f"Profile_{i}", + "is_active": True, + "is_deleted": False, + "rev": 1, + } + for i in range(100) + } + + result = get_active_profile_by_name(profiles_store, "Profile_42") + + assert result["profile_id"] == "uuid-42" + + def test_handles_mix_of_active_inactive_deleted(self): + """Test with mix of active, inactive, and deleted profiles.""" + profiles_store = { + "uuid-active": { + "profile_id": "uuid-active", + "name": "Target", + "is_active": True, + "is_deleted": False, + "rev": 1, + }, + "uuid-inactive": { + "profile_id": "uuid-inactive", + "name": "Target", + "is_active": False, + "is_deleted": False, + "rev": 2, # Higher rev but inactive + }, + "uuid-deleted": { + "profile_id": "uuid-deleted", + "name": "Target", + "is_active": True, + "is_deleted": True, + "rev": 3, # Highest rev but deleted + }, + } + + result = get_active_profile_by_name(profiles_store, "Target") + + # Should only find the active, non-deleted one + assert result["profile_id"] == "uuid-active" diff --git a/core/tests/test_search_engine.py b/core/tests/test_search_engine.py new file mode 100644 index 0000000..eced6d2 --- /dev/null +++ b/core/tests/test_search_engine.py @@ -0,0 +1,749 @@ +""" +Tier 3 Unit Tests for agent_core/rag/search_engine.py + +Tests the RAG search engine functionality: +- Database connection management with caching +- Search configuration loading and validation +- Vector similarity search +- Direct metadata search + +Test Categories: +1. Database Connections: get_db_connection() caching and upgrade logic +2. Config Loading: load_search_config(), get_search_config_details() +3. Vector Search: search_similar_documents() +4. Direct Search: direct_search() +""" + +import pytest +from unittest.mock import patch, MagicMock, mock_open +import yaml +import os +import tempfile + +from agent_core.rag.search_engine import ( + get_db_connection, + load_search_config, + get_search_config_details, + search_similar_documents, + direct_search, + _DB_CONNECTION_CACHE, + SCALAR_QUANTIZATION_LIMIT_8BIT, + SCALAR_QUANTIZATION_LIMIT_4BIT +) + + +class TestGetDbConnection: + """Tests for the get_db_connection function.""" + + def setup_method(self): + """Clear the connection cache before each test.""" + _DB_CONNECTION_CACHE.clear() + + def teardown_method(self): + """Clean up connections after each test.""" + for db_file, (conn, _) in list(_DB_CONNECTION_CACHE.items()): + try: + conn.close() + except: + pass + _DB_CONNECTION_CACHE.clear() + + @patch('agent_core.rag.search_engine.duckdb.connect') + def test_creates_new_connection(self, mock_connect): + """Test that a new connection is created when not cached.""" + mock_conn = MagicMock() + mock_connect.return_value = mock_conn + + result = get_db_connection("/path/to/db.duckdb", read_only=True) + + mock_connect.assert_called_once_with(database="/path/to/db.duckdb", read_only=True) + assert result is mock_conn + assert "/path/to/db.duckdb" in _DB_CONNECTION_CACHE + + @patch('agent_core.rag.search_engine.duckdb.connect') + def test_returns_cached_connection(self, mock_connect): + """Test that cached connections are returned.""" + mock_conn = MagicMock() + mock_connect.return_value = mock_conn + + # First call creates connection + conn1 = get_db_connection("/path/to/cached.db", read_only=True) + # Second call should return cached + conn2 = get_db_connection("/path/to/cached.db", read_only=True) + + assert mock_connect.call_count == 1 + assert conn1 is conn2 + + @patch('agent_core.rag.search_engine.duckdb.connect') + def test_upgrades_readonly_to_writable(self, mock_connect): + """Test that read-only connections are upgraded when write access is needed.""" + mock_ro_conn = MagicMock() + mock_rw_conn = MagicMock() + mock_connect.side_effect = [mock_ro_conn, mock_rw_conn] + + # First, get a read-only connection + conn1 = get_db_connection("/path/to/upgrade.db", read_only=True) + assert conn1 is mock_ro_conn + + # Now request a writable connection + conn2 = get_db_connection("/path/to/upgrade.db", read_only=False) + + # Should have closed the old connection and created a new one + mock_ro_conn.close.assert_called_once() + assert conn2 is mock_rw_conn + assert mock_connect.call_count == 2 + + @patch('agent_core.rag.search_engine.duckdb.connect') + def test_no_upgrade_needed_if_already_writable(self, mock_connect): + """Test that writable connections satisfy read-only requests.""" + mock_conn = MagicMock() + mock_connect.return_value = mock_conn + + # Get a writable connection first + get_db_connection("/path/to/writable.db", read_only=False) + # Request read-only - should reuse writable + conn2 = get_db_connection("/path/to/writable.db", read_only=True) + + assert mock_connect.call_count == 1 # No second connection created + assert conn2 is mock_conn + + @patch('agent_core.rag.search_engine.duckdb.connect') + def test_connection_error_raised(self, mock_connect): + """Test that connection errors are propagated.""" + import duckdb + mock_connect.side_effect = duckdb.Error("Connection failed") + + with pytest.raises(duckdb.Error, match="Connection failed"): + get_db_connection("/path/to/bad.db", read_only=True) + + +class TestLoadSearchConfig: + """Tests for the load_search_config function.""" + + def test_loads_valid_yaml(self, tmp_path): + """Test loading a valid YAML config file.""" + config_data = { + "database_file": "test.db", + "meta_table": {"name": "docs"}, + "embedding_table": {"name": "embeddings"} + } + config_file = tmp_path / "config.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = load_search_config(str(config_file)) + + assert result == config_data + + def test_file_not_found(self): + """Test that FileNotFoundError is raised for missing files.""" + with pytest.raises(FileNotFoundError): + load_search_config("/nonexistent/path/config.yaml") + + def test_invalid_yaml_syntax(self, tmp_path): + """Test that invalid YAML raises an error.""" + config_file = tmp_path / "invalid.yaml" + config_file.write_text("invalid: yaml: {[") + + with pytest.raises(yaml.YAMLError): + load_search_config(str(config_file)) + + def test_non_dict_yaml_raises(self, tmp_path): + """Test that non-dictionary YAML content raises ValueError.""" + config_file = tmp_path / "list.yaml" + config_file.write_text("- item1\n- item2") + + with pytest.raises(ValueError, match="not a valid YAML dictionary"): + load_search_config(str(config_file)) + + +class TestGetSearchConfigDetails: + """Tests for the get_search_config_details function.""" + + def test_complete_config(self, tmp_path): + """Test parsing a complete and valid config.""" + config_data = { + "description": "Test search config", + "database_writable": True, + "is_global": True, + "database_file": "test.duckdb", + "source_name": "test_source", + "meta_table": { + "name": "documents", + "id_column": "doc_id", + "text_column": "content", + "tags_column": "tags", + "retrieval_columns": ["title", "author"], + "direct_search_columns": ["category"] + }, + "embedding_table": { + "name": "embeddings", + "id_column": "doc_id", + "embedding_column": "vector", + "emb_model_id": "text-embedding-3-small", + "model_config_params": {"dimensions": 512}, + "quantization": "int8", + "mrl_dims": 256, + "query_task_type": "retrieval.query" + } + } + config_file = tmp_path / "complete.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result is not None + assert result["description"] == "Test search config" + assert result["database_writable"] is True + assert result["is_global"] is True + assert result["meta_table_name"] == "documents" + assert result["meta_id_col"] == "doc_id" + assert result["meta_text_col"] == "content" + assert result["emb_table_name"] == "embeddings" + assert result["emb_model_id"] == "text-embedding-3-small" + assert result["quantization"] == "int8" + assert result["emb_mrl_dims"] == 256 + # Database file should be resolved to absolute path + assert os.path.isabs(result["database_file"]) + + def test_relative_database_path_resolved(self, tmp_path): + """Test that relative database paths are resolved correctly.""" + config_data = { + "database_file": "data/mydb.duckdb", + "meta_table": {"name": "docs", "id_column": "id"}, + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec", + "emb_model_id": "model" + } + } + config_file = tmp_path / "relative.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + expected_db_path = os.path.join(str(tmp_path), "data/mydb.duckdb") + assert result["database_file"] == expected_db_path + + def test_absolute_database_path_unchanged(self, tmp_path): + """Test that absolute database paths are preserved.""" + config_data = { + "database_file": "/absolute/path/mydb.duckdb", + "meta_table": {"name": "docs", "id_column": "id"}, + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec", + "emb_model_id": "model" + } + } + config_file = tmp_path / "absolute.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result["database_file"] == "/absolute/path/mydb.duckdb" + + def test_missing_required_fields_returns_none(self, tmp_path): + """Test that missing required fields cause None return.""" + # Missing embedding table + config_data = { + "database_file": "test.db", + "meta_table": {"name": "docs", "id_column": "id"} + } + config_file = tmp_path / "incomplete.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result is None + + def test_missing_meta_id_column_returns_none(self, tmp_path): + """Test that missing meta_id_col returns None.""" + config_data = { + "database_file": "test.db", + "meta_table": {"name": "docs"}, # Missing id_column + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec", + "emb_model_id": "model" + } + } + config_file = tmp_path / "no_meta_id.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result is None + + def test_missing_emb_model_id_returns_none(self, tmp_path): + """Test that missing emb_model_id returns None.""" + config_data = { + "database_file": "test.db", + "meta_table": {"name": "docs", "id_column": "id"}, + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec" + # Missing emb_model_id + } + } + config_file = tmp_path / "no_model.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result is None + + def test_invalid_config_file_returns_none(self): + """Test that invalid config files return None.""" + result = get_search_config_details("/nonexistent/config.yaml") + assert result is None + + def test_retrieval_columns_merged_with_text_col(self, tmp_path): + """Test that retrieval columns include text column.""" + config_data = { + "database_file": "test.db", + "meta_table": { + "name": "docs", + "id_column": "id", + "text_column": "content", + "retrieval_columns": ["title", "content"] # Duplicate + }, + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec", + "emb_model_id": "model" + } + } + config_file = tmp_path / "retrieval.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + # Should have content first (as text_col), then title (without duplicate) + assert result["all_meta_retrieval_cols"] == ["content", "title"] + + def test_default_values(self, tmp_path): + """Test that default values are applied.""" + config_data = { + "database_file": "test.db", + "meta_table": {"name": "docs", "id_column": "id"}, + "embedding_table": { + "name": "emb", + "id_column": "id", + "embedding_column": "vec", + "emb_model_id": "model" + } + } + config_file = tmp_path / "defaults.yaml" + config_file.write_text(yaml.dump(config_data)) + + result = get_search_config_details(str(config_file)) + + assert result["database_writable"] is False # Default + assert result["is_global"] is False # Default + assert result["description"] == "No description provided." # Default + + +class TestSearchSimilarDocuments: + """Tests for the search_similar_documents function.""" + + def test_empty_query_returns_empty(self): + """Test that empty query returns empty results.""" + result = search_similar_documents( + query_text="", + search_config_details={}, + model_instance=MagicMock() + ) + assert result == [] + + def test_none_model_raises(self): + """Test that None model_instance raises ValueError.""" + with pytest.raises(ValueError, match="EmbeddingProvider instance must be provided"): + search_similar_documents( + query_text="test query", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "meta_tags_col": None, + "all_meta_retrieval_cols": [], + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": None, + "query_task_type": None + }, + model_instance=None + ) + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_embedding_generation_failure_returns_empty(self, mock_get_conn): + """Test that embedding generation failure returns empty results.""" + mock_model = MagicMock() + mock_model.generate_embedding.return_value = None + + result = search_similar_documents( + query_text="test query", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": ["content"], + "meta_tags_col": None, + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": 256, + "query_task_type": "retrieval.query", + "quantization": None + }, + model_instance=mock_model + ) + + assert result == [] + + @patch('agent_core.rag.search_engine.get_db_connection') + @patch('agent_core.rag.search_engine.fast_8bit_uniform_scalar_quantize') + def test_int8_quantization_applied(self, mock_quantize, mock_get_conn): + """Test that int8 quantization is applied when configured.""" + mock_model = MagicMock() + mock_model.generate_embedding.return_value = [[0.1, 0.2, 0.3]] + + mock_quantize.return_value = MagicMock() + mock_quantize.return_value.tolist.return_value = [[1, 2, 3]] + + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("content",), ("similarity",)] + mock_result.fetchall.return_value = [] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + search_similar_documents( + query_text="test", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": ["content"], + "meta_tags_col": None, + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": None, + "query_task_type": None, + "quantization": "int8" + }, + model_instance=mock_model + ) + + mock_quantize.assert_called_once() + + @patch('agent_core.rag.search_engine.get_db_connection') + @patch('agent_core.rag.search_engine.fast_4bit_uniform_scalar_quantize') + def test_int4_quantization_applied(self, mock_quantize, mock_get_conn): + """Test that int4 quantization is applied when configured.""" + mock_model = MagicMock() + mock_model.generate_embedding.return_value = [[0.1, 0.2, 0.3]] + + mock_quantize.return_value = MagicMock() + mock_quantize.return_value.tolist.return_value = [[1, 2, 3]] + + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("similarity",)] + mock_result.fetchall.return_value = [] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + search_similar_documents( + query_text="test", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "meta_tags_col": None, + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": None, + "query_task_type": None, + "quantization": "int4" + }, + model_instance=mock_model + ) + + mock_quantize.assert_called_once() + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_search_with_tags_filter(self, mock_get_conn): + """Test that tag filtering is applied in SQL.""" + import numpy as np + mock_model = MagicMock() + # Return numpy array to match expected format + mock_model.generate_embedding.return_value = np.array([[0.1, 0.2, 0.3]]) + + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("similarity",)] + mock_result.fetchall.return_value = [] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + search_similar_documents( + query_text="test", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "meta_tags_col": "tags", + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": None, + "query_task_type": None, + "quantization": None + }, + tags=["tag1", "tag2"], + model_instance=mock_model + ) + + # Check that SQL includes tag filter + call_args = mock_conn.execute.call_args + sql = call_args[0][0] + params = call_args[0][1] + assert "array_has_all" in sql + assert ["tag1", "tag2"] in params + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_search_returns_formatted_results(self, mock_get_conn): + """Test that search results are properly formatted as dicts.""" + import numpy as np + mock_model = MagicMock() + # Return numpy array to match expected format + mock_model.generate_embedding.return_value = np.array([[0.1, 0.2, 0.3]]) + + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("content",), ("similarity",)] + mock_result.fetchall.return_value = [ + ("doc1", "Test content 1", 0.95), + ("doc2", "Test content 2", 0.85) + ] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + results = search_similar_documents( + query_text="test", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": ["content"], + "meta_tags_col": None, + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": "vector", + "emb_mrl_dims": None, + "query_task_type": None, + "quantization": None + }, + top_k=10, + model_instance=mock_model + ) + + assert len(results) == 2 + assert results[0]["id"] == "doc1" + assert results[0]["content"] == "Test content 1" + assert results[0]["similarity"] == 0.95 + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_multiple_embedding_columns(self, mock_get_conn): + """Test search with multiple embedding columns uses GREATEST.""" + import numpy as np + mock_model = MagicMock() + # Return numpy array to match expected format + mock_model.generate_embedding.return_value = np.array([[0.1, 0.2, 0.3]]) + + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("similarity",)] + mock_result.fetchall.return_value = [] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + search_similar_documents( + query_text="test", + search_config_details={ + "database_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "meta_tags_col": None, + "emb_table_name": "emb", + "emb_id_col": "id", + "emb_vector_col": ["vec_title", "vec_content"], # Multiple columns + "emb_mrl_dims": None, + "query_task_type": None, + "quantization": None + }, + model_instance=mock_model + ) + + sql = mock_conn.execute.call_args[0][0] + assert "GREATEST" in sql + assert "vec_title" in sql + assert "vec_content" in sql + + +class TestDirectSearch: + """Tests for the direct_search function.""" + + def test_empty_query_params_returns_empty(self): + """Test that empty query params returns empty results.""" + result = direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "docs", + "all_meta_retrieval_cols": [], + "meta_id_col": "id" + }, + query_params={} + ) + assert result == [] + + def test_disallowed_column_returns_empty(self): + """Test that querying disallowed columns returns empty.""" + result = direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "direct_search_columns": ["allowed_col"] + }, + query_params={"disallowed_col": "value"} + ) + assert result == [] + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_valid_search_executes_sql(self, mock_get_conn): + """Test that valid searches execute proper SQL.""" + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("name",)] + mock_result.fetchall.return_value = [("1", "Doc 1"), ("2", "Doc 2")] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + results = direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "documents", + "meta_id_col": "id", + "all_meta_retrieval_cols": ["name"], + "direct_search_columns": ["category", "author"] + }, + query_params={"category": "tech"}, + limit=5 + ) + + assert len(results) == 2 + # Check SQL structure + sql = mock_conn.execute.call_args[0][0] + params = mock_conn.execute.call_args[0][1] + assert '"category" = ?' in sql + assert "tech" in params + assert 5 in params # limit + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_multiple_query_params(self, mock_get_conn): + """Test search with multiple query parameters.""" + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",)] + mock_result.fetchall.return_value = [] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "direct_search_columns": ["category", "status"] + }, + query_params={"category": "tech", "status": "active"} + ) + + sql = mock_conn.execute.call_args[0][0] + assert '"category" = ?' in sql + assert '"status" = ?' in sql + assert " AND " in sql + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_results_formatted_as_dicts(self, mock_get_conn): + """Test that results are returned as list of dicts.""" + mock_conn = MagicMock() + mock_result = MagicMock() + mock_result.description = [("id",), ("title",), ("content",)] + mock_result.fetchall.return_value = [ + ("doc1", "Title 1", "Content 1"), + ("doc2", "Title 2", "Content 2") + ] + mock_conn.execute.return_value = mock_result + mock_get_conn.return_value = mock_conn + + results = direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": ["title", "content"], + "direct_search_columns": ["category"] + }, + query_params={"category": "test"} + ) + + assert results[0] == {"id": "doc1", "title": "Title 1", "content": "Content 1"} + assert results[1] == {"id": "doc2", "title": "Title 2", "content": "Content 2"} + + @patch('agent_core.rag.search_engine.get_db_connection') + def test_duckdb_error_returns_empty(self, mock_get_conn): + """Test that DuckDB errors are handled gracefully.""" + import duckdb + mock_conn = MagicMock() + mock_conn.execute.side_effect = duckdb.Error("Query failed") + mock_get_conn.return_value = mock_conn + + result = direct_search( + search_config_details={ + "db_file": "test.db", + "meta_table_name": "docs", + "meta_id_col": "id", + "all_meta_retrieval_cols": [], + "direct_search_columns": ["col"] + }, + query_params={"col": "val"} + ) + + assert result == [] + + +class TestScalarQuantizationLimits: + """Tests for scalar quantization limit constants.""" + + def test_8bit_limit_value(self): + """Test that 8-bit limit is correctly defined.""" + assert SCALAR_QUANTIZATION_LIMIT_8BIT == 0.3 + + def test_4bit_limit_value(self): + """Test that 4-bit limit is correctly defined.""" + assert SCALAR_QUANTIZATION_LIMIT_4BIT == 0.18 diff --git a/core/tests/test_session_resilience_handlers.py b/core/tests/test_session_resilience_handlers.py new file mode 100644 index 0000000..b58bed4 --- /dev/null +++ b/core/tests/test_session_resilience_handlers.py @@ -0,0 +1,303 @@ +""" +Unit tests for session resilience message handlers. + +Tests cover: +- handle_reconnect_message +- handle_heartbeat_message +- Integration with connection_manager +""" +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from datetime import datetime, timezone + +import sys +from pathlib import Path +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + + +@pytest.fixture +def mock_event_manager(): + """Create a mock SessionEventManager.""" + manager = AsyncMock() + manager.session_id = "test-session-123" + manager.emit_raw = AsyncMock() + return manager + + +@pytest.fixture +def mock_websocket(): + """Create a mock WebSocket.""" + ws = MagicMock() + ws.state = MagicMock() + return ws + + +@pytest.fixture +def mock_ws_state(mock_event_manager, mock_websocket): + """Create a mock websocket state object.""" + state = MagicMock() + state.event_manager = mock_event_manager + state.session_id = "test-session-123" + state.websocket = mock_websocket + state.active_run_id = None + return state + + +class TestHandleReconnectMessage: + """Test the reconnect message handler.""" + + @pytest.mark.asyncio + async def test_missing_run_id_returns_error(self, mock_ws_state): + """Test that missing run_id returns an error.""" + from api.message_handlers import handle_reconnect_message + + data = {} # No run_id + + await handle_reconnect_message(mock_ws_state, data) + + # Should emit an error + mock_ws_state.event_manager.emit_raw.assert_called_once() + call_args = mock_ws_state.event_manager.emit_raw.call_args + assert call_args[0][0] == "reconnect_error" + assert "Missing run_id" in call_args[0][1]["error"] + + @pytest.mark.asyncio + async def test_run_not_found_returns_error(self, mock_ws_state): + """Test that non-existent run returns an error.""" + from api.message_handlers import handle_reconnect_message + from api.session import active_runs_store + + # Ensure run doesn't exist + run_id = "nonexistent-run-id" + if run_id in active_runs_store: + del active_runs_store[run_id] + + data = {"run_id": run_id, "last_event_id": 0} + + await handle_reconnect_message(mock_ws_state, data) + + # Should emit an error + mock_ws_state.event_manager.emit_raw.assert_called_once() + call_args = mock_ws_state.event_manager.emit_raw.call_args + assert call_args[0][0] == "reconnect_error" + assert "not found" in call_args[0][1]["error"].lower() + + @pytest.mark.asyncio + async def test_successful_reconnect(self, mock_ws_state): + """Test successful reconnection to an existing run.""" + from api.message_handlers import handle_reconnect_message + from api.session import active_runs_store + from api.connection_manager import connection_manager + + # Set up an existing run + run_id = "test-run-for-reconnect" + active_runs_store[run_id] = { + "meta": {"run_id": run_id, "status": "running"}, + "team_state": {}, + } + + # Mock the connection manager reconnect + with patch.object( + connection_manager, + "reconnect_run", + new=AsyncMock(return_value={ + "success": True, + "buffered_events": [], + "events_replayed": 0, + }) + ): + data = {"run_id": run_id, "last_event_id": 42} + + await handle_reconnect_message(mock_ws_state, data) + + # Should emit reconnected message + mock_ws_state.event_manager.emit_raw.assert_called_once() + call_args = mock_ws_state.event_manager.emit_raw.call_args + assert call_args[0][0] == "reconnected" + assert call_args[0][1]["run_id"] == run_id + + # Cleanup + del active_runs_store[run_id] + + +class TestHandleHeartbeatMessage: + """Test the heartbeat message handler.""" + + @pytest.mark.asyncio + async def test_heartbeat_sends_ack(self, mock_ws_state): + """Test that heartbeat sends acknowledgment.""" + from api.message_handlers import handle_heartbeat_message + from api.connection_manager import connection_manager + + # Mock handle_client_heartbeat + with patch.object( + connection_manager, + "handle_client_heartbeat", + new=AsyncMock() + ): + data = { + "timestamp": 1703779200000, + "session_id": "test-session-123", + "run_id": "test-run-id", + } + + await handle_heartbeat_message(mock_ws_state, data) + + # Should emit heartbeat_ack + mock_ws_state.event_manager.emit_raw.assert_called_once() + call_args = mock_ws_state.event_manager.emit_raw.call_args + assert call_args[0][0] == "heartbeat_ack" + ack_data = call_args[0][1] + assert ack_data["timestamp"] == 1703779200000 + assert "serverTime" in ack_data + assert "sessionValid" in ack_data + + @pytest.mark.asyncio + async def test_heartbeat_validates_session_id(self, mock_ws_state): + """Test that heartbeat validates session ID.""" + from api.message_handlers import handle_heartbeat_message + from api.connection_manager import connection_manager + + with patch.object( + connection_manager, + "handle_client_heartbeat", + new=AsyncMock() + ): + # Mismatched session ID + data = { + "timestamp": 1703779200000, + "session_id": "wrong-session-id", + "run_id": "test-run-id", + } + + await handle_heartbeat_message(mock_ws_state, data) + + # Should still respond but indicate session invalid + call_args = mock_ws_state.event_manager.emit_raw.call_args + ack_data = call_args[0][1] + assert ack_data["sessionValid"] is False + + @pytest.mark.asyncio + async def test_heartbeat_handles_connection_manager_error(self, mock_ws_state): + """Test that heartbeat handles connection manager errors gracefully.""" + from api.message_handlers import handle_heartbeat_message + from api.connection_manager import connection_manager + + with patch.object( + connection_manager, + "handle_client_heartbeat", + new=AsyncMock(side_effect=Exception("Test error")) + ): + data = { + "timestamp": 1703779200000, + "session_id": "test-session-123", + } + + # Should not raise, should still send ack + await handle_heartbeat_message(mock_ws_state, data) + + # Should still emit heartbeat_ack despite error + mock_ws_state.event_manager.emit_raw.assert_called_once() + + +class TestMessageHandlerRegistry: + """Test that handlers are properly registered.""" + + def test_reconnect_handler_registered(self): + """Test that reconnect handler is in MESSAGE_HANDLERS.""" + from api.message_handlers import MESSAGE_HANDLERS + + assert "reconnect" in MESSAGE_HANDLERS + assert callable(MESSAGE_HANDLERS["reconnect"]) + + def test_heartbeat_handler_registered(self): + """Test that heartbeat handler is in MESSAGE_HANDLERS.""" + from api.message_handlers import MESSAGE_HANDLERS + + assert "heartbeat" in MESSAGE_HANDLERS + assert callable(MESSAGE_HANDLERS["heartbeat"]) + + +class TestSessionResilienceIntegration: + """Integration tests for session resilience flow.""" + + @pytest.mark.asyncio + async def test_full_reconnection_flow(self, mock_ws_state): + """Test a full reconnection flow from disconnect to reconnect.""" + from api.message_handlers import handle_reconnect_message + from api.session import active_runs_store + from api.connection_manager import connection_manager + + run_id = "integration-test-run" + session_id = "test-session-123" + + # Setup: Create an active run + active_runs_store[run_id] = { + "meta": {"run_id": run_id, "status": "running"}, + "team_state": {"work_modules": {}}, + } + + try: + # Step 1: Register connection (simulated) + connection_manager.register_connection( + session_id=session_id, + websocket=mock_ws_state.websocket, + event_manager=mock_ws_state.event_manager, + ) + + # Step 2: Register run with session + connection_manager.register_run( + session_id=session_id, + run_id=run_id, + ) + + # Step 3: Simulate disconnect + runs_in_grace = connection_manager.unregister_connection(session_id) + + # Should be in grace period (returns list of run_ids) + assert run_id in runs_in_grace + + # Step 4: Create new session and reconnect + new_session_id = "new-session-456" + new_event_manager = AsyncMock() + new_event_manager.session_id = new_session_id + new_event_manager.emit_raw = AsyncMock() + + mock_ws_state.session_id = new_session_id + mock_ws_state.event_manager = new_event_manager + + connection_manager.register_connection( + session_id=new_session_id, + websocket=mock_ws_state.websocket, + event_manager=new_event_manager, + ) + + # Step 5: Send reconnect message + reconnect_data = { + "run_id": run_id, + "last_event_id": 0, + } + + await handle_reconnect_message(mock_ws_state, reconnect_data) + + # Verify reconnect response + new_event_manager.emit_raw.assert_called() + call_args = new_event_manager.emit_raw.call_args + # Check it was a reconnect response (could be success or error) + msg_type = call_args[0][0] + assert msg_type in ["reconnected", "reconnect_error"] + + finally: + # Cleanup + if run_id in active_runs_store: + del active_runs_store[run_id] + # Clean up connections + try: + await connection_manager.unregister_connection(session_id) + except: + pass + try: + await connection_manager.unregister_connection("new-session-456") + except: + pass diff --git a/core/tests/test_session_security.py b/core/tests/test_session_security.py new file mode 100644 index 0000000..617d790 --- /dev/null +++ b/core/tests/test_session_security.py @@ -0,0 +1,526 @@ +""" +Unit tests for session security (JWT + fingerprint cookie binding). + +Tests cover: +- Token creation and validation +- Fingerprint binding (OWASP Token Sidejacking Prevention) +- Refresh token rotation (Auth0 pattern) +- Token revocation +- Edge cases and error handling +""" +import pytest +import time +import jwt +from datetime import datetime, timedelta, timezone +from unittest.mock import patch, MagicMock + +import sys +from pathlib import Path +CORE_DIR = Path(__file__).parent.parent +sys.path.insert(0, str(CORE_DIR)) + +from api.session_security import ( + SessionSecurityConfig, + SessionSecurityManager, + SessionTokens, + RefreshResult, + ValidationResult, + TokenPayload, + init_security_manager, + get_security_manager, +) + + +@pytest.fixture +def security_config(): + """Create a test security configuration.""" + return SessionSecurityConfig( + jwt_secret="test_secret_key_that_is_at_least_64_characters_long_for_security", + jwt_expiry_minutes=15, + refresh_token_expiry_hours=24, + jwt_issuer="commonground", + jwt_audience="commonground-ws-reconnect", + fingerprint_cookie_secure=False, # Allow HTTP for testing + ) + + +@pytest.fixture +def security_manager(security_config): + """Create a SessionSecurityManager for testing.""" + return SessionSecurityManager(security_config) + + +class TestSessionTokenCreation: + """Test JWT and session token creation.""" + + def test_create_session_tokens_returns_valid_structure(self, security_manager): + """Test that token creation returns all required fields.""" + tokens = security_manager.create_session_tokens( + session_id="test-session-123", + project_id="test-project", + ) + + assert isinstance(tokens, SessionTokens) + assert tokens.session_id == "test-session-123" + assert tokens.jwt_token is not None + assert len(tokens.jwt_token) > 0 + assert tokens.refresh_token is not None + assert len(tokens.refresh_token) > 0 + assert tokens.fingerprint is not None + assert len(tokens.fingerprint) > 0 + assert tokens.expires_in == 15 * 60 # 15 minutes in seconds + + def test_jwt_contains_required_claims(self, security_manager, security_config): + """Test that JWT contains all required RFC 7519 claims.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Decode without verification to inspect claims + payload = jwt.decode( + tokens.jwt_token, + security_config.jwt_secret, + algorithms=[security_config.jwt_algorithm], + audience=security_config.jwt_audience, + ) + + # Standard claims + assert payload["iss"] == "commonground" + assert payload["sub"] == "test-session" + assert payload["aud"] == "commonground-ws-reconnect" + assert "exp" in payload + assert "iat" in payload + assert "nbf" in payload + assert "jti" in payload + + # Custom claims + assert payload["pid"] == "test-project" + assert "ver" in payload + assert "fph" in payload # Fingerprint hash + + def test_jwt_header_contains_explicit_type(self, security_manager): + """Test that JWT header includes typ: session+jwt (RFC 8725 Section 3.11).""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Get headers without verification + headers = jwt.get_unverified_header(tokens.jwt_token) + assert headers.get("typ") == "session+jwt" + + def test_fingerprint_is_cryptographically_random(self, security_manager): + """Test that fingerprints are unique and properly random.""" + tokens1 = security_manager.create_session_tokens( + session_id="session-1", + project_id="project-1", + ) + tokens2 = security_manager.create_session_tokens( + session_id="session-2", + project_id="project-1", + ) + + assert tokens1.fingerprint != tokens2.fingerprint + assert len(tokens1.fingerprint) >= 32 # At least 32 characters + + def test_existing_fingerprint_reuse_for_reconnection(self, security_manager): + """Test that existing fingerprint can be reused for reconnection.""" + original_fingerprint = "existing_fingerprint_value" + + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + existing_fingerprint=original_fingerprint, + ) + + assert tokens.fingerprint == original_fingerprint + + +class TestJWTValidation: + """Test JWT validation with fingerprint binding.""" + + def test_valid_jwt_with_correct_fingerprint(self, security_manager): + """Test successful validation with matching fingerprint.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + + assert result.valid is True + assert result.session_id == "test-session" + assert result.project_id == "test-project" + assert result.error is None + + def test_invalid_fingerprint_rejected(self, security_manager): + """Test that mismatched fingerprint is rejected (OWASP Sidejacking).""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie="wrong_fingerprint_value", + ) + + assert result.valid is False + assert "Fingerprint mismatch" in result.error + + def test_missing_fingerprint_rejected(self, security_manager): + """Test that missing fingerprint is rejected.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=None, + ) + + assert result.valid is False + assert "Missing fingerprint cookie" in result.error + + def test_expired_jwt_rejected(self, security_manager, security_config): + """Test that expired JWT is rejected.""" + # Create a token with past expiry + now = datetime.now(timezone.utc) + past_exp = int((now - timedelta(hours=1)).timestamp()) + + payload = { + "iss": security_config.jwt_issuer, + "sub": "test-session", + "aud": security_config.jwt_audience, + "exp": past_exp, + "iat": int((now - timedelta(hours=2)).timestamp()), + "nbf": int((now - timedelta(hours=2)).timestamp()), + "jti": "test-jti", + "pid": "test-project", + "ver": 1, + "fph": "test-hash", + } + + expired_token = jwt.encode( + payload, + security_config.jwt_secret, + algorithm=security_config.jwt_algorithm, + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=expired_token, + fingerprint_cookie="any", + ) + + assert result.valid is False + assert "expired" in result.error.lower() + + def test_invalid_signature_rejected(self, security_manager): + """Test that JWT with invalid signature is rejected.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Modify the token to invalidate signature + modified_token = tokens.jwt_token[:-5] + "XXXXX" + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=modified_token, + fingerprint_cookie=tokens.fingerprint, + ) + + assert result.valid is False + + def test_wrong_audience_rejected(self, security_manager, security_config): + """Test that JWT with wrong audience is rejected (RFC 8725).""" + now = datetime.now(timezone.utc) + + payload = { + "iss": security_config.jwt_issuer, + "sub": "test-session", + "aud": "wrong-audience", # Wrong! + "exp": int((now + timedelta(hours=1)).timestamp()), + "iat": int(now.timestamp()), + "nbf": int(now.timestamp()), + "jti": "test-jti", + "pid": "test-project", + "ver": 1, + "fph": "test-hash", + } + + wrong_aud_token = jwt.encode( + payload, + security_config.jwt_secret, + algorithm=security_config.jwt_algorithm, + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=wrong_aud_token, + fingerprint_cookie="any", + ) + + assert result.valid is False + assert "audience" in result.error.lower() + + +class TestRefreshTokenRotation: + """Test refresh token rotation (Auth0 pattern).""" + + def test_successful_token_refresh(self, security_manager): + """Test successful refresh returns new tokens.""" + original_tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + refresh_result = security_manager.refresh_tokens( + refresh_token=original_tokens.refresh_token, + fingerprint_cookie=original_tokens.fingerprint, + ) + + assert refresh_result is not None + assert isinstance(refresh_result, RefreshResult) + assert refresh_result.jwt_token != original_tokens.jwt_token + assert refresh_result.refresh_token != original_tokens.refresh_token + assert refresh_result.expires_in > 0 + + def test_refresh_token_rotation_single_use(self, security_manager): + """Test that refresh token can only be used once (rotation).""" + original_tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # First refresh should succeed + first_refresh = security_manager.refresh_tokens( + refresh_token=original_tokens.refresh_token, + fingerprint_cookie=original_tokens.fingerprint, + ) + assert first_refresh is not None + + # Second use of same token should fail (reuse detection) + second_refresh = security_manager.refresh_tokens( + refresh_token=original_tokens.refresh_token, + fingerprint_cookie=original_tokens.fingerprint, + ) + assert second_refresh is None + + def test_refresh_with_wrong_fingerprint_fails(self, security_manager): + """Test that refresh fails with wrong fingerprint.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + result = security_manager.refresh_tokens( + refresh_token=tokens.refresh_token, + fingerprint_cookie="wrong_fingerprint", + ) + + assert result is None + + def test_refresh_with_invalid_token_fails(self, security_manager): + """Test that refresh fails with invalid token.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + result = security_manager.refresh_tokens( + refresh_token="invalid_refresh_token", + fingerprint_cookie=tokens.fingerprint, + ) + + assert result is None + + +class TestSessionRevocation: + """Test session and token revocation.""" + + def test_revoke_session_invalidates_existing_jwt(self, security_manager): + """Test that session revocation invalidates all JWTs for that session.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Verify token is valid before revocation + pre_revoke = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + assert pre_revoke.valid is True + + # Revoke the session + security_manager.revoke_session("test-session") + + # Token should now be invalid (version mismatch) + post_revoke = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + assert post_revoke.valid is False + assert "version" in post_revoke.error.lower() + + def test_revoke_session_removes_refresh_tokens(self, security_manager): + """Test that session revocation removes all refresh tokens.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Revoke the session + security_manager.revoke_session("test-session") + + # Refresh should fail (token removed) + result = security_manager.refresh_tokens( + refresh_token=tokens.refresh_token, + fingerprint_cookie=tokens.fingerprint, + ) + assert result is None + + def test_revoke_specific_jwt_by_jti(self, security_manager, security_config): + """Test revoking a specific JWT by its JTI.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Extract JTI from token + payload = jwt.decode( + tokens.jwt_token, + security_config.jwt_secret, + algorithms=[security_config.jwt_algorithm], + audience=security_config.jwt_audience, + ) + jti = payload["jti"] + + # Revoke by JTI + security_manager.revoke_token(jti) + + # Token should now be invalid + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + assert result.valid is False + assert "revoked" in result.error.lower() + + +class TestCookieSettings: + """Test cookie configuration.""" + + def test_cookie_settings_structure(self, security_manager): + """Test that cookie settings contain required security attributes.""" + settings = security_manager.get_cookie_settings() + + assert "key" in settings + assert settings["key"] == "__Secure-Fgp" + assert settings["httponly"] is True + assert settings["samesite"] == "Strict" + assert "max_age" in settings + assert "path" in settings + + +class TestCleanup: + """Test cleanup of expired tokens.""" + + def test_cleanup_expired_refresh_tokens(self, security_manager): + """Test that expired refresh tokens are cleaned up.""" + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id="test-project", + ) + + # Manually expire the token + token_hash = security_manager._hash_refresh_token(tokens.refresh_token) + security_manager._refresh_tokens[token_hash].expires_at = ( + datetime.now(timezone.utc) - timedelta(hours=1) + ) + + # Run cleanup + cleaned = security_manager.cleanup_expired() + + assert cleaned == 1 + assert token_hash not in security_manager._refresh_tokens + + +class TestTokenPayloadModel: + """Test TokenPayload Pydantic model.""" + + def test_token_payload_validation(self): + """Test that TokenPayload validates correctly.""" + now = int(datetime.now(timezone.utc).timestamp()) + + payload = TokenPayload( + iss="commonground", + sub="test-session", + aud="commonground-ws-reconnect", + exp=now + 900, + iat=now, + nbf=now, + jti="unique-jti", + pid="test-project", + ver=1, + fph="fingerprint-hash", + ) + + assert payload.iss == "commonground" + assert payload.sub == "test-session" + assert payload.ver == 1 + + +class TestEdgeCases: + """Test edge cases and error handling.""" + + def test_empty_session_id(self, security_manager): + """Test handling of empty session ID.""" + tokens = security_manager.create_session_tokens( + session_id="", + project_id="test-project", + ) + + # Should still work, but session_id will be empty + assert tokens.session_id == "" + + def test_very_long_project_id(self, security_manager): + """Test handling of very long project ID.""" + long_project_id = "x" * 1000 + + tokens = security_manager.create_session_tokens( + session_id="test-session", + project_id=long_project_id, + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + + assert result.valid is True + assert result.project_id == long_project_id + + def test_unicode_in_ids(self, security_manager): + """Test handling of unicode characters in IDs.""" + unicode_session = "session-ζ—₯本θͺž-πŸŽ‰" + unicode_project = "project-δΈ­ζ–‡-Γ©moji" + + tokens = security_manager.create_session_tokens( + session_id=unicode_session, + project_id=unicode_project, + ) + + result = security_manager.validate_jwt_with_fingerprint( + jwt_token=tokens.jwt_token, + fingerprint_cookie=tokens.fingerprint, + ) + + assert result.valid is True + assert result.session_id == unicode_session + assert result.project_id == unicode_project diff --git a/core/tests/test_stale_session_detection.py b/core/tests/test_stale_session_detection.py new file mode 100644 index 0000000..269bd88 --- /dev/null +++ b/core/tests/test_stale_session_detection.py @@ -0,0 +1,243 @@ +""" +Tests for stale/orphaned session detection in GetPrincipalStatusSummaryTool. + +These tests verify that the Partner agent correctly identifies sessions that: +1. Have modules stuck in "ongoing" status without recent updates +2. Appear to be orphaned (all active modules are stale) +3. Were likely interrupted by WebSocket disconnects +""" + +import pytest +from datetime import datetime, timezone, timedelta +from unittest.mock import MagicMock, AsyncMock, patch +import sys +import os + +# Add the core directory to the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from agent_core.nodes.custom_nodes.get_principal_status_tool import ( + GetPrincipalStatusSummaryTool, + STALE_ONGOING_THRESHOLD_MINUTES, + ORPHANED_SESSION_THRESHOLD_MINUTES +) + + +class TestStaleModuleDetection: + """Tests for the _detect_stale_modules helper method.""" + + @pytest.fixture + def tool(self): + """Create a GetPrincipalStatusSummaryTool instance.""" + return GetPrincipalStatusSummaryTool() + + @pytest.fixture + def now(self): + """Current timestamp for testing.""" + return datetime.now(timezone.utc) + + def test_no_stale_modules_when_recently_updated(self, tool, now): + """Modules updated within threshold should not be flagged as stale.""" + recent_update = (now - timedelta(minutes=5)).isoformat() + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": recent_update}, + "WM_2": {"status": "ongoing", "updated_at": recent_update}, + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 0 + assert is_orphaned is False + assert warning == "" + + def test_detects_single_stale_module(self, tool, now): + """A single module past the threshold should be detected.""" + old_update = (now - timedelta(minutes=STALE_ONGOING_THRESHOLD_MINUTES + 5)).isoformat() + recent_update = (now - timedelta(minutes=2)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, + "WM_2": {"status": "ongoing", "updated_at": recent_update}, + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 1 + assert stale_modules[0]["module_id"] == "WM_1" + assert is_orphaned is False # Not all active modules are stale + assert "WARNING" in warning + assert "WM_1" in warning + + def test_detects_orphaned_session_all_modules_stale(self, tool, now): + """When ALL active modules are stale, session should be flagged as orphaned.""" + old_update = (now - timedelta(minutes=STALE_ONGOING_THRESHOLD_MINUTES + 30)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, + "WM_2": {"status": "ongoing", "updated_at": old_update}, + "WM_3": {"status": "ongoing", "updated_at": old_update}, + "WM_4": {"status": "pending", "updated_at": old_update}, # Not active, ignored + "WM_5": {"status": "completed", "updated_at": old_update}, # Not active, ignored + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 3 # Only ongoing modules + assert is_orphaned is True + assert "CRITICAL" in warning + assert "ORPHANED" in warning + + def test_ignores_completed_modules(self, tool, now): + """Completed modules should not be checked for staleness.""" + old_update = (now - timedelta(hours=24)).isoformat() + + work_modules = { + "WM_1": {"status": "completed", "updated_at": old_update}, + "WM_2": {"status": "pending_review", "updated_at": old_update}, + "WM_3": {"status": "pending", "updated_at": old_update}, + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 0 + assert is_orphaned is False + assert warning == "" + + def test_handles_missing_updated_at(self, tool, now): + """Modules without updated_at should not crash.""" + work_modules = { + "WM_1": {"status": "ongoing"}, # No updated_at + "WM_2": {"status": "ongoing", "updated_at": None}, + } + + # Should not raise + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + # Can't determine staleness without timestamp, so not flagged + assert len(stale_modules) == 0 + + def test_handles_invalid_timestamp_format(self, tool, now): + """Invalid timestamp formats should be handled gracefully.""" + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": "not-a-timestamp"}, + "WM_2": {"status": "ongoing", "updated_at": "2025-13-45T99:99:99"}, # Invalid + } + + # Should not raise + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + # Invalid timestamps can't be evaluated, so not flagged + assert len(stale_modules) == 0 + + def test_handles_timezone_naive_timestamps(self, tool, now): + """Timestamps without timezone info should be handled.""" + # Timezone-naive timestamp (will be treated as UTC) + old_update_naive = (now - timedelta(minutes=STALE_ONGOING_THRESHOLD_MINUTES + 10)).replace(tzinfo=None).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update_naive}, + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 1 + + def test_very_old_session_hours_ago(self, tool, now): + """Sessions from hours ago should be clearly identified.""" + # Simulate the tunneling-festive-mammoth scenario: ~27 hours old + very_old_update = (now - timedelta(hours=27)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": very_old_update}, + "WM_2": {"status": "ongoing", "updated_at": very_old_update}, + "WM_3": {"status": "ongoing", "updated_at": very_old_update}, + "WM_4": {"status": "ongoing", "updated_at": very_old_update}, + "WM_5": {"status": "ongoing", "updated_at": very_old_update}, + "WM_6": {"status": "pending", "updated_at": very_old_update}, + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert len(stale_modules) == 5 # Only the 5 ongoing modules + assert is_orphaned is True + assert "CRITICAL" in warning + assert "ORPHANED" in warning + + # Check that minutes are calculated correctly (should be ~1620 minutes) + max_minutes = max(m["minutes_since_update"] for m in stale_modules) + assert max_minutes > 1600 # ~27 hours = 1620 minutes + + def test_mixed_status_modules(self, tool, now): + """Test with a mix of ongoing, completed, and pending modules.""" + old_update = (now - timedelta(minutes=STALE_ONGOING_THRESHOLD_MINUTES + 20)).isoformat() + recent_update = (now - timedelta(minutes=2)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, # Stale + "WM_2": {"status": "completed", "updated_at": old_update}, # Ignored + "WM_3": {"status": "pending_review", "updated_at": old_update}, # Ignored + "WM_4": {"status": "in_progress", "updated_at": recent_update}, # Active but recent + "WM_5": {"status": "pending", "updated_at": old_update}, # Ignored + } + + stale_modules, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + # Only WM_1 should be stale (WM_4 is recent) + assert len(stale_modules) == 1 + assert stale_modules[0]["module_id"] == "WM_1" + assert is_orphaned is False # WM_4 is still active and not stale + + +class TestWarningMessageFormat: + """Tests for warning message formatting.""" + + @pytest.fixture + def tool(self): + return GetPrincipalStatusSummaryTool() + + @pytest.fixture + def now(self): + return datetime.now(timezone.utc) + + def test_warning_includes_module_ids(self, tool, now): + """Warning message should list affected module IDs.""" + old_update = (now - timedelta(minutes=60)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, + "WM_2": {"status": "ongoing", "updated_at": old_update}, + } + + _, _, warning = tool._detect_stale_modules(work_modules, now) + + assert "WM_1" in warning + assert "WM_2" in warning + + def test_warning_includes_time_duration(self, tool, now): + """Warning should include how long modules have been stale.""" + old_update = (now - timedelta(minutes=45)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, + } + + _, _, warning = tool._detect_stale_modules(work_modules, now) + + assert "45" in warning # Should mention ~45 minutes + + def test_orphaned_warning_mentions_websocket(self, tool, now): + """Orphaned session warning should mention WebSocket disconnect.""" + old_update = (now - timedelta(hours=2)).isoformat() + + work_modules = { + "WM_1": {"status": "ongoing", "updated_at": old_update}, + } + + _, is_orphaned, warning = tool._detect_stale_modules(work_modules, now) + + assert is_orphaned is True + assert "WebSocket" in warning or "disconnect" in warning.lower() + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/core/tests/test_token_counter.py b/core/tests/test_token_counter.py new file mode 100644 index 0000000..c9d0f3b --- /dev/null +++ b/core/tests/test_token_counter.py @@ -0,0 +1,313 @@ +""" +Unit tests for agent_core/llm/token_counter.py + +Tests provider-aware token counting with Anthropic API and litellm fallback. +""" + +import pytest +from unittest.mock import patch, MagicMock + +from agent_core.llm.token_counter import ( + count_tokens, + _is_anthropic_model, + _normalize_model_for_anthropic, + _convert_messages_for_anthropic, + _count_tokens_litellm, + _detect_provider, + _normalize_model_name, + LLMProvider, + PROVIDER_TOKEN_COUNTERS, +) + + +class TestDetectProvider: + """Tests for _detect_provider function.""" + + def test_anthropic_direct_models(self): + """Test detection of direct Claude model names.""" + assert _detect_provider("claude-3-sonnet-20240229") == LLMProvider.ANTHROPIC + assert _detect_provider("claude-sonnet-4-20250514") == LLMProvider.ANTHROPIC + assert _detect_provider("claude-3-opus-20240229") == LLMProvider.ANTHROPIC + + def test_anthropic_prefixed_models(self): + """Test detection of provider-prefixed Claude models.""" + assert _detect_provider("anthropic/claude-3-sonnet") == LLMProvider.ANTHROPIC + assert _detect_provider("bedrock/anthropic.claude-3-sonnet") == LLMProvider.ANTHROPIC + + def test_openai_models(self): + """Test detection of OpenAI models.""" + assert _detect_provider("gpt-4") == LLMProvider.OPENAI + assert _detect_provider("gpt-3.5-turbo") == LLMProvider.OPENAI + assert _detect_provider("o1-preview") == LLMProvider.OPENAI + assert _detect_provider("openai/gpt-4") == LLMProvider.OPENAI + + def test_google_models(self): + """Test detection of Google/Gemini models.""" + assert _detect_provider("gemini-pro") == LLMProvider.GOOGLE + assert _detect_provider("gemini-1.5-pro") == LLMProvider.GOOGLE + assert _detect_provider("google/gemini-pro") == LLMProvider.GOOGLE + assert _detect_provider("vertex_ai/gemini-pro") == LLMProvider.GOOGLE + + def test_unknown_models(self): + """Test unknown models return UNKNOWN.""" + assert _detect_provider("llama-3-70b") == LLMProvider.UNKNOWN + assert _detect_provider("mistral-7b") == LLMProvider.UNKNOWN + assert _detect_provider("") == LLMProvider.UNKNOWN + assert _detect_provider(None) == LLMProvider.UNKNOWN + + +class TestIsAnthropicModel: + """Tests for _is_anthropic_model helper (backward compat).""" + + def test_direct_claude_models(self): + """Test detection of direct Claude model names.""" + assert _is_anthropic_model("claude-3-sonnet-20240229") is True + assert _is_anthropic_model("claude-sonnet-4-20250514") is True + assert _is_anthropic_model("claude-3-opus-20240229") is True + assert _is_anthropic_model("claude-3-haiku-20240307") is True + + def test_prefixed_claude_models(self): + """Test detection of provider-prefixed Claude models.""" + assert _is_anthropic_model("anthropic/claude-3-sonnet") is True + assert _is_anthropic_model("bedrock/anthropic.claude-3-sonnet") is True + + def test_non_claude_models(self): + """Test non-Claude models return False.""" + assert _is_anthropic_model("gpt-4") is False + assert _is_anthropic_model("gpt-3.5-turbo") is False + assert _is_anthropic_model("gemini-pro") is False + assert _is_anthropic_model("") is False + assert _is_anthropic_model(None) is False + + +class TestNormalizeModelName: + """Tests for _normalize_model_name helper.""" + + def test_strips_anthropic_prefix(self): + """Test stripping anthropic/ prefix.""" + assert _normalize_model_name("anthropic/claude-3-sonnet", LLMProvider.ANTHROPIC) == "claude-3-sonnet" + + def test_strips_bedrock_prefix(self): + """Test stripping bedrock/anthropic. prefix.""" + assert _normalize_model_name("bedrock/anthropic.claude-3-sonnet", LLMProvider.ANTHROPIC) == "claude-3-sonnet" + + def test_strips_openai_prefix(self): + """Test stripping openai/ prefix.""" + assert _normalize_model_name("openai/gpt-4", LLMProvider.OPENAI) == "gpt-4" + + def test_strips_google_prefix(self): + """Test stripping google/ prefix.""" + assert _normalize_model_name("google/gemini-pro", LLMProvider.GOOGLE) == "gemini-pro" + assert _normalize_model_name("vertex_ai/gemini-pro", LLMProvider.GOOGLE) == "gemini-pro" + + def test_leaves_bare_model_unchanged(self): + """Test bare model names pass through.""" + assert _normalize_model_name("claude-3-sonnet", LLMProvider.ANTHROPIC) == "claude-3-sonnet" + assert _normalize_model_name("gpt-4", LLMProvider.OPENAI) == "gpt-4" + + +class TestNormalizeModelForAnthropic: + """Tests for _normalize_model_for_anthropic helper (backward compat).""" + + def test_strips_anthropic_prefix(self): + """Test stripping anthropic/ prefix.""" + assert _normalize_model_for_anthropic("anthropic/claude-3-sonnet") == "claude-3-sonnet" + + def test_strips_bedrock_prefix(self): + """Test stripping bedrock/anthropic. prefix.""" + assert _normalize_model_for_anthropic("bedrock/anthropic.claude-3-sonnet") == "claude-3-sonnet" + + def test_leaves_bare_model_unchanged(self): + """Test bare model names pass through.""" + assert _normalize_model_for_anthropic("claude-3-sonnet") == "claude-3-sonnet" + assert _normalize_model_for_anthropic("claude-sonnet-4-20250514") == "claude-sonnet-4-20250514" + + +class TestConvertMessagesForAnthropic: + """Tests for _convert_messages_for_anthropic helper.""" + + def test_extracts_system_message(self): + """Test system message extraction.""" + messages = [ + {"role": "system", "content": "You are helpful"}, + {"role": "user", "content": "Hello"} + ] + + system, anthropic_msgs = _convert_messages_for_anthropic(messages) + + assert system == "You are helpful" + assert len(anthropic_msgs) == 1 + assert anthropic_msgs[0]["role"] == "user" + + def test_combines_system_prompts(self): + """Test combining explicit system_prompt with system message.""" + messages = [ + {"role": "system", "content": "Be concise"}, + {"role": "user", "content": "Hello"} + ] + + system, _ = _convert_messages_for_anthropic(messages, system_prompt="You are helpful") + + assert "You are helpful" in system + assert "Be concise" in system + + def test_converts_user_assistant_messages(self): + """Test user/assistant messages pass through.""" + messages = [ + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"} + ] + + _, anthropic_msgs = _convert_messages_for_anthropic(messages) + + assert len(anthropic_msgs) == 2 + assert anthropic_msgs[0] == {"role": "user", "content": "Hello"} + assert anthropic_msgs[1] == {"role": "assistant", "content": "Hi there!"} + + def test_converts_tool_messages(self): + """Test tool result messages are converted to user messages.""" + messages = [ + {"role": "tool", "tool_call_id": "call_123", "content": "Result data"} + ] + + _, anthropic_msgs = _convert_messages_for_anthropic(messages) + + assert len(anthropic_msgs) == 1 + assert anthropic_msgs[0]["role"] == "user" + assert anthropic_msgs[0]["content"][0]["type"] == "tool_result" + assert anthropic_msgs[0]["content"][0]["tool_use_id"] == "call_123" + + +class TestCountTokensLitellm: + """Tests for _count_tokens_litellm fallback.""" + + @patch("agent_core.llm.token_counter.litellm.token_counter") + def test_calls_litellm(self, mock_counter): + """Test litellm is called correctly.""" + mock_counter.return_value = 50 + + result = _count_tokens_litellm("gpt-4", [{"role": "user", "content": "Hello"}]) + + assert result == 50 + mock_counter.assert_called_once() + + @patch("agent_core.llm.token_counter.litellm.token_counter") + def test_handles_exception(self, mock_counter): + """Test graceful handling of litellm exceptions.""" + mock_counter.side_effect = Exception("Failed") + + result = _count_tokens_litellm("gpt-4", [{"role": "user", "content": "Hello"}]) + + assert result == 0 + + +class TestCountTokens: + """Tests for the main count_tokens function.""" + + @patch("agent_core.llm.token_counter._count_tokens_litellm") + def test_uses_litellm_for_non_anthropic(self, mock_litellm): + """Test non-Anthropic models use litellm.""" + mock_litellm.return_value = 25 + + result = count_tokens(model="gpt-4", text="Hello world") + + assert result == 25 + mock_litellm.assert_called_once() + + def test_uses_anthropic_for_claude(self): + """Test Claude models use Anthropic API when available.""" + mock_anthropic_fn = MagicMock(return_value=30) + + # Patch the dict entry directly since PROVIDER_TOKEN_COUNTERS holds function refs + with patch.dict( + "agent_core.llm.token_counter.PROVIDER_TOKEN_COUNTERS", + {LLMProvider.ANTHROPIC: mock_anthropic_fn} + ): + result = count_tokens(model="claude-sonnet-4-20250514", text="Hello world") + + assert result == 30 + mock_anthropic_fn.assert_called_once() + + def test_falls_back_to_litellm_on_anthropic_failure(self): + """Test fallback to litellm when Anthropic API fails.""" + mock_anthropic_fn = MagicMock(return_value=None) # Indicates failure + mock_litellm_fn = MagicMock(return_value=20) + + with patch.dict( + "agent_core.llm.token_counter.PROVIDER_TOKEN_COUNTERS", + {LLMProvider.ANTHROPIC: mock_anthropic_fn} + ): + with patch("agent_core.llm.token_counter._count_tokens_litellm", mock_litellm_fn): + result = count_tokens(model="claude-3-sonnet", text="Hello") + + assert result == 20 + mock_anthropic_fn.assert_called_once() + mock_litellm_fn.assert_called_once() + + def test_returns_zero_for_no_model(self): + """Test missing model returns 0.""" + result = count_tokens(model="", text="Hello") + assert result == 0 + + def test_returns_zero_for_empty_input(self): + """Test empty input returns 0.""" + result = count_tokens(model="gpt-4") + assert result == 0 + + def test_raises_for_both_text_and_messages(self): + """Test providing both text and messages raises ValueError.""" + with pytest.raises(ValueError, match="not both"): + count_tokens( + model="gpt-4", + text="Hello", + messages=[{"role": "user", "content": "World"}] + ) + + @patch("agent_core.llm.token_counter._count_tokens_litellm") + def test_uses_override_model_from_config(self, mock_litellm): + """Test litellm_token_counter_model override is respected.""" + mock_litellm.return_value = 15 + config = {"litellm_token_counter_model": "gpt-3.5-turbo"} + + count_tokens(model="custom-model", text="Hello", llm_config=config) + + # Should use the override model + call_args = mock_litellm.call_args + assert call_args[0][0] == "gpt-3.5-turbo" + + def test_includes_system_prompt_in_messages(self): + """Test system_prompt is included in message count.""" + mock_anthropic_fn = MagicMock(return_value=40) + + with patch.dict( + "agent_core.llm.token_counter.PROVIDER_TOKEN_COUNTERS", + {LLMProvider.ANTHROPIC: mock_anthropic_fn} + ): + count_tokens( + model="claude-3-sonnet", + text="Hello", + system_prompt="You are helpful" + ) + + call_args = mock_anthropic_fn.call_args + messages = call_args[1]["messages"] + # System prompt should be in the messages + assert any(m.get("role") == "system" for m in messages) + + +class TestCountTokensIntegration: + """Integration tests that test the full flow (can be skipped in CI without API keys).""" + + @pytest.mark.skipif( + True, # Set to False to run integration tests locally + reason="Integration tests require API keys" + ) + def test_anthropic_api_call(self): + """Test actual Anthropic API call (requires ANTHROPIC_API_KEY).""" + result = count_tokens( + model="claude-sonnet-4-20250514", + messages=[{"role": "user", "content": "Hello, how are you?"}] + ) + + assert result > 0 + assert isinstance(result, int) diff --git a/core/tests/test_tool_registry.py b/core/tests/test_tool_registry.py new file mode 100644 index 0000000..8641e18 --- /dev/null +++ b/core/tests/test_tool_registry.py @@ -0,0 +1,615 @@ +""" +Unit tests for agent_core/framework/tool_registry.py + +Tests tool registration, retrieval, and formatting functions. +""" + +import pytest +from unittest.mock import patch, MagicMock +from pathlib import Path + +from agent_core.framework.tool_registry import ( + _TOOL_REGISTRY, + _sanitize_schema_for_api, + tool_registry, + get_registered_tools, + get_tool_by_name, + get_tool_node_class, + get_tools_by_toolset_names, + get_all_toolsets_with_tools, + format_tools_for_llm_api, + format_tools_for_prompt, + format_tools_for_prompt_by_toolset, + format_simplified_tools_for_prompt_by_toolset, + register_native_mcp_tool, +) + + +@pytest.fixture(autouse=True) +def clear_registry(): + """Clear the tool registry before and after each test.""" + _TOOL_REGISTRY.clear() + yield + _TOOL_REGISTRY.clear() + + +class TestSanitizeSchemaForApi: + """Tests for the _sanitize_schema_for_api function.""" + + def test_removes_x_prefixed_keys(self): + """Test that keys starting with x- are removed.""" + schema = { + "type": "object", + "x-custom": "should be removed", + "properties": { + "name": {"type": "string", "x-internal": True} + } + } + + result = _sanitize_schema_for_api(schema) + + assert "x-custom" not in result + assert "x-internal" not in result["properties"]["name"] + assert result["type"] == "object" + assert result["properties"]["name"]["type"] == "string" + + def test_handles_nested_dicts(self): + """Test that nested dictionaries are processed recursively.""" + schema = { + "properties": { + "outer": { + "x-metadata": "remove", + "properties": { + "inner": {"x-deep": True, "type": "number"} + } + } + } + } + + result = _sanitize_schema_for_api(schema) + + assert "x-metadata" not in result["properties"]["outer"] + assert "x-deep" not in result["properties"]["outer"]["properties"]["inner"] + + def test_handles_lists(self): + """Test that lists are processed recursively.""" + schema = { + "oneOf": [ + {"type": "string", "x-option": 1}, + {"type": "number", "x-option": 2} + ] + } + + result = _sanitize_schema_for_api(schema) + + assert len(result["oneOf"]) == 2 + assert "x-option" not in result["oneOf"][0] + assert "x-option" not in result["oneOf"][1] + + def test_preserves_primitives(self): + """Test that primitive values are preserved.""" + schema = {"type": "string", "minLength": 1, "maxLength": 100} + + result = _sanitize_schema_for_api(schema) + + assert result == schema + + def test_handles_empty_dict(self): + """Test handling of empty dictionary.""" + result = _sanitize_schema_for_api({}) + + assert result == {} + + def test_handles_none(self): + """Test handling of None value.""" + result = _sanitize_schema_for_api(None) + + assert result is None + + +class TestToolRegistryDecorator: + """Tests for the @tool_registry decorator.""" + + def test_registers_tool(self): + """Test that decorator registers a tool in the registry.""" + from pocketflow import BaseNode + + @tool_registry( + name="test_tool", + description="A test tool", + parameters={"type": "object", "properties": {}} + ) + class TestTool(BaseNode): + pass + + assert "test_tool" in _TOOL_REGISTRY + assert _TOOL_REGISTRY["test_tool"]["name"] == "test_tool" + assert _TOOL_REGISTRY["test_tool"]["description"] == "A test tool" + + def test_stores_node_class(self): + """Test that decorator stores the node class reference.""" + from pocketflow import BaseNode + + @tool_registry( + name="test_tool_class", + description="Test", + parameters={} + ) + class TestToolClass(BaseNode): + pass + + assert _TOOL_REGISTRY["test_tool_class"]["node_class"] is TestToolClass + + def test_sets_tool_info_attribute(self): + """Test that decorator sets _tool_info attribute on class.""" + from pocketflow import BaseNode + + @tool_registry( + name="tool_with_attr", + description="Test", + parameters={} + ) + class ToolWithAttr(BaseNode): + pass + + assert hasattr(ToolWithAttr, "_tool_info") + assert ToolWithAttr._tool_info["name"] == "tool_with_attr" + + def test_raises_for_non_basenode(self): + """Test that decorator raises TypeError for non-BaseNode classes.""" + with pytest.raises(TypeError, match="BaseNode"): + @tool_registry( + name="invalid_tool", + description="Test", + parameters={} + ) + class NotANode: + pass + + def test_ends_flow_default_false(self): + """Test that ends_flow defaults to False.""" + from pocketflow import BaseNode + + @tool_registry( + name="normal_tool", + description="Test", + parameters={} + ) + class NormalTool(BaseNode): + pass + + assert _TOOL_REGISTRY["normal_tool"]["ends_flow"] is False + + def test_ends_flow_set_true(self): + """Test that ends_flow can be set to True.""" + from pocketflow import BaseNode + + @tool_registry( + name="final_tool", + description="Test", + parameters={}, + ends_flow=True + ) + class FinalTool(BaseNode): + pass + + assert _TOOL_REGISTRY["final_tool"]["ends_flow"] is True + + def test_toolset_name_defaults_to_tool_name(self): + """Test that toolset_name defaults to the tool name.""" + from pocketflow import BaseNode + + @tool_registry( + name="standalone_tool", + description="Test", + parameters={} + ) + class StandaloneTool(BaseNode): + pass + + assert _TOOL_REGISTRY["standalone_tool"]["toolset_name"] == "standalone_tool" + + def test_custom_toolset_name(self): + """Test setting a custom toolset name.""" + from pocketflow import BaseNode + + @tool_registry( + name="grouped_tool", + description="Test", + parameters={}, + toolset_name="my_toolset" + ) + class GroupedTool(BaseNode): + pass + + assert _TOOL_REGISTRY["grouped_tool"]["toolset_name"] == "my_toolset" + + def test_implementation_type_is_internal(self): + """Test that implementation_type is set to 'internal'.""" + from pocketflow import BaseNode + + @tool_registry( + name="internal_tool", + description="Test", + parameters={} + ) + class InternalTool(BaseNode): + pass + + assert _TOOL_REGISTRY["internal_tool"]["implementation_type"] == "internal" + + +class TestGetRegisteredTools: + """Tests for get_registered_tools function.""" + + def test_returns_empty_list_when_no_tools(self): + """Test returns empty list when registry is empty.""" + result = get_registered_tools() + + assert result == [] + + def test_returns_all_registered_tools(self): + """Test returns all tools in registry.""" + _TOOL_REGISTRY["tool1"] = {"name": "tool1"} + _TOOL_REGISTRY["tool2"] = {"name": "tool2"} + + result = get_registered_tools() + + assert len(result) == 2 + names = [t["name"] for t in result] + assert "tool1" in names + assert "tool2" in names + + +class TestGetToolByName: + """Tests for get_tool_by_name function.""" + + def test_returns_tool_when_found(self): + """Test returns tool info when name exists.""" + _TOOL_REGISTRY["my_tool"] = {"name": "my_tool", "description": "Test"} + + result = get_tool_by_name("my_tool") + + assert result["name"] == "my_tool" + assert result["description"] == "Test" + + def test_returns_none_when_not_found(self): + """Test returns None when tool doesn't exist.""" + result = get_tool_by_name("nonexistent") + + assert result is None + + +class TestGetToolNodeClass: + """Tests for get_tool_node_class function.""" + + def test_returns_node_class(self): + """Test returns the node class for a tool.""" + class MockNode: + pass + + _TOOL_REGISTRY["class_tool"] = {"name": "class_tool", "node_class": MockNode} + + result = get_tool_node_class("class_tool") + + assert result is MockNode + + def test_returns_none_for_missing_tool(self): + """Test returns None when tool doesn't exist.""" + result = get_tool_node_class("nonexistent") + + assert result is None + + +class TestGetToolsByToolsetNames: + """Tests for get_tools_by_toolset_names function.""" + + def test_returns_empty_for_empty_list(self): + """Test returns empty list for empty toolset names.""" + result = get_tools_by_toolset_names([]) + + assert result == [] + + def test_returns_tools_matching_toolset(self): + """Test returns tools matching the specified toolsets.""" + _TOOL_REGISTRY["tool_a"] = {"name": "tool_a", "toolset_name": "set1"} + _TOOL_REGISTRY["tool_b"] = {"name": "tool_b", "toolset_name": "set2"} + _TOOL_REGISTRY["tool_c"] = {"name": "tool_c", "toolset_name": "set1"} + + result = get_tools_by_toolset_names(["set1"]) + + assert len(result) == 2 + names = [t["name"] for t in result] + assert "tool_a" in names + assert "tool_c" in names + assert "tool_b" not in names + + def test_returns_tools_from_multiple_toolsets(self): + """Test returns tools from multiple specified toolsets.""" + _TOOL_REGISTRY["tool_a"] = {"name": "tool_a", "toolset_name": "set1"} + _TOOL_REGISTRY["tool_b"] = {"name": "tool_b", "toolset_name": "set2"} + _TOOL_REGISTRY["tool_c"] = {"name": "tool_c", "toolset_name": "set3"} + + result = get_tools_by_toolset_names(["set1", "set2"]) + + assert len(result) == 2 + + +class TestGetAllToolsetsWithTools: + """Tests for get_all_toolsets_with_tools function.""" + + def test_returns_empty_dict_when_no_tools(self): + """Test returns empty dict when registry is empty.""" + result = get_all_toolsets_with_tools() + + assert result == {} + + def test_groups_tools_by_toolset(self): + """Test groups tools by their toolset name.""" + _TOOL_REGISTRY["tool_a"] = {"name": "tool_a", "toolset_name": "set1", "description": "A"} + _TOOL_REGISTRY["tool_b"] = {"name": "tool_b", "toolset_name": "set1", "description": "B"} + _TOOL_REGISTRY["tool_c"] = {"name": "tool_c", "toolset_name": "set2", "description": "C"} + + result = get_all_toolsets_with_tools() + + assert "set1" in result + assert "set2" in result + assert len(result["set1"]) == 2 + assert len(result["set2"]) == 1 + + +class TestFormatToolsForLlmApi: + """Tests for format_tools_for_llm_api function.""" + + def test_formats_tool_for_api(self): + """Test formats tool in OpenAI function format.""" + tools = [{ + "name": "search", + "description": "Search for information", + "toolset_name": "search_tools", + "parameters": {"type": "object", "properties": {"query": {"type": "string"}}} + }] + + result = format_tools_for_llm_api(tools) + + assert len(result) == 1 + assert result[0]["type"] == "function" + assert result[0]["function"]["name"] == "search" + assert "search_tools" in result[0]["function"]["description"] + + def test_sanitizes_parameters(self): + """Test that x- prefixed keys are removed from parameters.""" + tools = [{ + "name": "tool", + "description": "Test", + "toolset_name": "test", + "parameters": { + "type": "object", + "x-internal": "remove", + "properties": {"arg": {"type": "string", "x-meta": True}} + } + }] + + result = format_tools_for_llm_api(tools) + + params = result[0]["function"]["parameters"] + assert "x-internal" not in params + assert "x-meta" not in params["properties"]["arg"] + + def test_appends_toolset_to_description(self): + """Test that toolset name is appended to description.""" + tools = [{ + "name": "my_tool", + "description": "Does something", + "toolset_name": "my_toolset", + "parameters": {} + }] + + result = format_tools_for_llm_api(tools) + + assert "(Belongs to toolset: 'my_toolset')" in result[0]["function"]["description"] + + def test_handles_non_dict_parameters(self): + """Test handling of invalid non-dict parameters.""" + tools = [{ + "name": "broken_tool", + "description": "Test", + "toolset_name": "test", + "parameters": "invalid" # Should be dict + }] + + result = format_tools_for_llm_api(tools) + + # Should use empty object as fallback + assert result[0]["function"]["parameters"]["type"] == "object" + + +class TestFormatToolsForPrompt: + """Tests for format_tools_for_prompt function.""" + + def test_formats_tools_as_markdown(self): + """Test formats tools as Markdown text.""" + tools = [ + {"name": "tool1", "description": "First tool"}, + {"name": "tool2", "description": "Second tool"} + ] + + result = format_tools_for_prompt(tools) + + assert "### Registered Tools" in result + assert "**tool1**" in result + assert "First tool" in result + assert "**tool2**" in result + + def test_handles_empty_list(self): + """Test handles empty tools list.""" + result = format_tools_for_prompt([]) + + assert "### Registered Tools" in result + + +class TestFormatToolsForPromptByToolset: + """Tests for format_tools_for_prompt_by_toolset function.""" + + def test_groups_tools_by_toolset(self): + """Test groups tools by toolset in output.""" + tools_by_toolset = { + "search": [{"name": "web_search", "description": "Search web"}], + "files": [{"name": "read_file", "description": "Read a file"}] + } + + result = format_tools_for_prompt_by_toolset(tools_by_toolset) + + assert "#### Toolset: search" in result + assert "#### Toolset: files" in result + assert "**web_search**" in result + assert "**read_file**" in result + + def test_skips_empty_toolsets(self): + """Test skips toolsets with no tools.""" + tools_by_toolset = { + "full": [{"name": "tool", "description": "Test"}], + "empty": [] + } + + result = format_tools_for_prompt_by_toolset(tools_by_toolset) + + assert "Toolset: full" in result + assert "Toolset: empty" not in result + + +class TestFormatSimplifiedToolsForPromptByToolset: + """Tests for format_simplified_tools_for_prompt_by_toolset function.""" + + def test_includes_reference_header(self): + """Test includes header about associate agent tools.""" + tools_by_toolset = {"test": [{"name": "tool", "description": "Desc"}]} + + result = format_simplified_tools_for_prompt_by_toolset(tools_by_toolset) + + assert "Associate Agent Available Tools Reference" in result + assert "You cannot call these directly" in result + + +class TestRegisterNativeMcpTool: + """Tests for register_native_mcp_tool function.""" + + def test_registers_mcp_tool(self): + """Test registers a native MCP tool.""" + register_native_mcp_tool( + name="mcp_search", + description="Search via MCP", + parameters={"type": "object", "properties": {}}, + server_name="search_server" + ) + + # Name should be prefixed with server name + assert "search_server_mcp_search" in _TOOL_REGISTRY + tool = _TOOL_REGISTRY["search_server_mcp_search"] + assert tool["original_name"] == "mcp_search" + assert tool["implementation_type"] == "native_mcp" + assert tool["mcp_server_name"] == "search_server" + + def test_uses_server_name_as_toolset(self): + """Test that server name is used as toolset name.""" + register_native_mcp_tool( + name="tool", + description="Test", + parameters={}, + server_name="my_server" + ) + + assert _TOOL_REGISTRY["my_server_tool"]["toolset_name"] == "my_server" + + def test_skips_invalid_parameters(self): + """Test skips registration for non-dict parameters.""" + register_native_mcp_tool( + name="bad_tool", + description="Test", + parameters="invalid", # Not a dict + server_name="server" + ) + + assert "server_bad_tool" not in _TOOL_REGISTRY + + def test_stores_knowledge_item_type(self): + """Test stores default_knowledge_item_type.""" + register_native_mcp_tool( + name="kb_tool", + description="Test", + parameters={}, + server_name="server", + default_knowledge_item_type="DOCUMENT" + ) + + assert _TOOL_REGISTRY["server_kb_tool"]["default_knowledge_item_type"] == "DOCUMENT" + + def test_stores_output_field_mappings(self): + """Test stores source_uri and title field mappings.""" + register_native_mcp_tool( + name="output_tool", + description="Test", + parameters={}, + server_name="server", + source_uri_field_in_output="url", + title_field_in_output="name" + ) + + tool = _TOOL_REGISTRY["server_output_tool"] + assert tool["source_uri_field_in_output"] == "url" + assert tool["title_field_in_output"] == "name" + + def test_overwrites_existing_tool_with_warning(self): + """Test overwrites existing tool with same name.""" + register_native_mcp_tool( + name="dupe", + description="First", + parameters={}, + server_name="server" + ) + register_native_mcp_tool( + name="dupe", + description="Second", + parameters={}, + server_name="server" + ) + + assert _TOOL_REGISTRY["server_dupe"]["description"] == "Second" + + +class TestToolRegistryIntegration: + """Integration tests for tool registry functionality.""" + + def test_full_workflow(self): + """Test complete workflow of registering and retrieving tools.""" + from pocketflow import BaseNode + + # Register internal tool + @tool_registry( + name="internal_search", + description="Internal search", + parameters={"type": "object", "properties": {"q": {"type": "string"}}}, + toolset_name="search" + ) + class InternalSearch(BaseNode): + pass + + # Register MCP tool + register_native_mcp_tool( + name="external_search", + description="External search", + parameters={"type": "object", "properties": {}}, + server_name="search" # Same toolset + ) + + # Get by toolset + tools = get_tools_by_toolset_names(["search"]) + assert len(tools) == 2 + + # Format for LLM + api_tools = format_tools_for_llm_api(tools) + assert len(api_tools) == 2 + + # Get node class + node_class = get_tool_node_class("internal_search") + assert node_class is InternalSearch diff --git a/core/tests/test_turn_manager.py b/core/tests/test_turn_manager.py new file mode 100644 index 0000000..e165e73 --- /dev/null +++ b/core/tests/test_turn_manager.py @@ -0,0 +1,538 @@ +""" +Unit tests for agent_core.framework.turn_manager module. + +This module tests the TurnManager class which manages the lifecycle +of Agent Turns - the atomic units of agent execution tracking. + +Key functionality tested: +- Turn creation and initialization +- LLM interaction tracking +- Tool interaction recording +- Turn finalization and error handling +- Flow ID management for execution tracing +- Special turns (delimiter, aggregation) +""" + +import pytest +import uuid +from datetime import datetime, timezone +from unittest.mock import patch, MagicMock +from agent_core.framework.turn_manager import TurnManager + + +class TestTurnManagerHelpers: + """Tests for TurnManager helper methods.""" + + @pytest.fixture + def turn_manager(self): + """Create a TurnManager instance.""" + return TurnManager() + + @pytest.fixture + def team_state_with_turns(self): + """Create team_state with sample turns.""" + return { + "turns": [ + {"turn_id": "turn_1", "status": "completed"}, + {"turn_id": "turn_2", "status": "completed"}, + {"turn_id": "turn_3", "status": "running"}, + ] + } + + def test_get_turn_by_id_finds_turn(self, turn_manager, team_state_with_turns): + """Test _get_turn_by_id finds existing turn.""" + turn = turn_manager._get_turn_by_id(team_state_with_turns, "turn_2") + assert turn is not None + assert turn["turn_id"] == "turn_2" + + def test_get_turn_by_id_returns_none_for_missing(self, turn_manager, team_state_with_turns): + """Test _get_turn_by_id returns None for missing turn.""" + turn = turn_manager._get_turn_by_id(team_state_with_turns, "nonexistent") + assert turn is None + + def test_get_turn_by_id_handles_empty_turns(self, turn_manager): + """Test _get_turn_by_id handles empty turns list.""" + team_state = {"turns": []} + turn = turn_manager._get_turn_by_id(team_state, "any_id") + assert turn is None + + def test_get_turn_by_id_handles_missing_turns_key(self, turn_manager): + """Test _get_turn_by_id handles missing turns key.""" + team_state = {} + turn = turn_manager._get_turn_by_id(team_state, "any_id") + assert turn is None + + def test_get_turn_by_id_handles_none_turn_id(self, turn_manager, team_state_with_turns): + """Test _get_turn_by_id handles None turn_id.""" + turn = turn_manager._get_turn_by_id(team_state_with_turns, None) + assert turn is None + + def test_get_turn_by_id_searches_reverse(self, turn_manager): + """Test _get_turn_by_id searches from most recent (reverse order).""" + # Same turn_id appears twice (shouldn't happen, but tests reverse search) + team_state = { + "turns": [ + {"turn_id": "dup", "value": "first"}, + {"turn_id": "dup", "value": "second"}, + ] + } + turn = turn_manager._get_turn_by_id(team_state, "dup") + assert turn["value"] == "second" # Returns the later one + + +class TestAddTurn: + """Tests for add_turn method.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + def test_add_turn_to_empty_team_state(self, turn_manager): + """Test adding turn to team_state without existing turns.""" + team_state = {} + turn_object = {"turn_id": "new_turn", "turn_type": "agent_turn"} + + turn_manager.add_turn(team_state, turn_object) + + assert "turns" in team_state + assert len(team_state["turns"]) == 1 + assert team_state["turns"][0]["turn_id"] == "new_turn" + + def test_add_turn_appends_to_existing(self, turn_manager): + """Test adding turn appends to existing turns list.""" + team_state = {"turns": [{"turn_id": "existing"}]} + turn_object = {"turn_id": "new_turn", "turn_type": "agent_turn"} + + turn_manager.add_turn(team_state, turn_object) + + assert len(team_state["turns"]) == 2 + assert team_state["turns"][-1]["turn_id"] == "new_turn" + + +class TestStartNewTurn: + """Tests for start_new_turn method.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + @pytest.fixture + def sample_context(self): + """Create a sample context for testing.""" + return { + "meta": { + "agent_id": "test-agent", + "run_id": "run-123", + }, + "state": { + "last_turn_id": None, + }, + "loaded_profile": { + "name": "TestProfile", + "profile_id": "profile-uuid", + }, + "refs": { + "team": {"turns": []}, + }, + } + + def test_start_new_turn_creates_turn(self, turn_manager, sample_context): + """Test start_new_turn creates a new turn.""" + stream_id = "stream-abc" + + turn_id = turn_manager.start_new_turn(sample_context, stream_id) + + assert turn_id is not None + assert turn_id.startswith("turn_test-agent_") + assert len(sample_context["refs"]["team"]["turns"]) == 1 + + def test_start_new_turn_sets_current_turn_id(self, turn_manager, sample_context): + """Test start_new_turn sets current_turn_id in state.""" + turn_id = turn_manager.start_new_turn(sample_context, "stream-1") + + assert sample_context["state"]["current_turn_id"] == turn_id + + def test_start_new_turn_populates_agent_info(self, turn_manager, sample_context): + """Test start_new_turn populates agent_info correctly.""" + turn_manager.start_new_turn(sample_context, "stream-1") + + turn = sample_context["refs"]["team"]["turns"][0] + assert turn["agent_info"]["agent_id"] == "test-agent" + assert turn["agent_info"]["profile_logical_name"] == "TestProfile" + assert turn["agent_info"]["profile_instance_id"] == "profile-uuid" + + def test_start_new_turn_initializes_llm_interaction(self, turn_manager, sample_context): + """Test start_new_turn initializes LLM interaction with stream_id.""" + stream_id = "stream-xyz" + turn_manager.start_new_turn(sample_context, stream_id) + + turn = sample_context["refs"]["team"]["turns"][0] + assert turn["llm_interaction"]["status"] == "running" + assert turn["llm_interaction"]["attempts"][0]["stream_id"] == stream_id + assert turn["llm_interaction"]["attempts"][0]["status"] == "pending" + + def test_start_new_turn_links_to_previous(self, turn_manager, sample_context): + """Test start_new_turn links to previous turn via source_turn_ids.""" + # Add a previous turn + sample_context["state"]["last_turn_id"] = "prev-turn-id" + sample_context["refs"]["team"]["turns"] = [ + {"turn_id": "prev-turn-id", "flow_id": "flow-existing"} + ] + + turn_manager.start_new_turn(sample_context, "stream-1") + + new_turn = sample_context["refs"]["team"]["turns"][-1] + assert new_turn["source_turn_ids"] == ["prev-turn-id"] + assert new_turn["flow_id"] == "flow-existing" # Inherits flow_id + + def test_start_new_turn_creates_new_flow_if_no_previous(self, turn_manager, sample_context): + """Test start_new_turn creates new flow_id when no previous turn.""" + turn_manager.start_new_turn(sample_context, "stream-1") + + turn = sample_context["refs"]["team"]["turns"][0] + assert turn["flow_id"].startswith("flow_root_") + assert turn["source_turn_ids"] == [] + + def test_start_new_turn_status_is_running(self, turn_manager, sample_context): + """Test start_new_turn sets status to running.""" + turn_manager.start_new_turn(sample_context, "stream-1") + + turn = sample_context["refs"]["team"]["turns"][0] + assert turn["status"] == "running" + assert turn["end_time"] is None + + +class TestToolInteractions: + """Tests for tool interaction tracking.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + @pytest.fixture + def context_with_turn(self): + """Context with an active turn.""" + turn = { + "turn_id": "turn-active", + "tool_interactions": [], + } + return { + "state": {"current_turn_id": "turn-active"}, + "refs": {"team": {"turns": [turn]}}, + "meta": {"agent_id": "test-agent"}, + } + + def test_add_tool_interaction(self, turn_manager, context_with_turn): + """Test adding a tool interaction.""" + tool_call = { + "id": "call-123", + "function": { + "name": "search_tool", + "arguments": '{"query": "test"}', + }, + } + + turn_manager.add_tool_interaction(context_with_turn, tool_call) + + turn = context_with_turn["refs"]["team"]["turns"][0] + assert len(turn["tool_interactions"]) == 1 + ti = turn["tool_interactions"][0] + assert ti["tool_call_id"] == "call-123" + assert ti["tool_name"] == "search_tool" + assert ti["status"] == "running" + assert ti["input_params"] == {"query": "test"} + + def test_update_tool_interaction_result_success(self, turn_manager, context_with_turn): + """Test updating tool interaction with success result.""" + # Add a running tool interaction first + context_with_turn["refs"]["team"]["turns"][0]["tool_interactions"] = [ + {"tool_call_id": "call-1", "status": "running"} + ] + + turn_manager.update_tool_interaction_result( + context_with_turn, "call-1", {"result": "success"}, is_error=False + ) + + ti = context_with_turn["refs"]["team"]["turns"][0]["tool_interactions"][0] + assert ti["status"] == "completed" + assert ti["result_payload"] == {"result": "success"} + assert ti["end_time"] is not None + + def test_update_tool_interaction_result_error(self, turn_manager, context_with_turn): + """Test updating tool interaction with error result.""" + context_with_turn["refs"]["team"]["turns"][0]["tool_interactions"] = [ + {"tool_call_id": "call-err", "status": "running"} + ] + + turn_manager.update_tool_interaction_result( + context_with_turn, "call-err", "Connection failed", is_error=True + ) + + ti = context_with_turn["refs"]["team"]["turns"][0]["tool_interactions"][0] + assert ti["status"] == "error" + assert ti["error_details"] == "Connection failed" + + def test_record_failed_tool_interaction(self, turn_manager, context_with_turn): + """Test recording an immediately failed tool interaction.""" + tool_call = { + "id": "call-invalid", + "function": {"name": "unknown_tool"}, + } + + turn_manager.record_failed_tool_interaction( + context_with_turn, tool_call, "Tool not found" + ) + + turn = context_with_turn["refs"]["team"]["turns"][0] + ti = turn["tool_interactions"][0] + assert ti["status"] == "error" + assert ti["error_details"] == "Tool not found" + assert ti["start_time"] == ti["end_time"] # Immediate failure + + +class TestLLMInteraction: + """Tests for LLM interaction tracking.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + @pytest.fixture + def context_with_llm_turn(self): + """Context with turn having LLM interaction.""" + turn = { + "turn_id": "turn-llm", + "llm_interaction": { + "status": "running", + "attempts": [{"stream_id": "s1", "status": "pending", "error": None}], + "final_response": None, + "actual_usage": None, + }, + } + return { + "state": {"current_turn_id": "turn-llm"}, + "refs": {"team": {"turns": [turn]}}, + } + + def test_update_llm_interaction_end_success(self, turn_manager, context_with_llm_turn): + """Test updating LLM interaction on successful completion.""" + llm_response = { + "content": "Here is the answer", + "tool_calls": None, + "reasoning": "I analyzed the question", + "model_id_used": "gpt-4", + "actual_usage": {"prompt_tokens": 100, "completion_tokens": 50}, + } + + turn_manager.update_llm_interaction_end(context_with_llm_turn, llm_response) + + llm_int = context_with_llm_turn["refs"]["team"]["turns"][0]["llm_interaction"] + assert llm_int["status"] == "completed" + assert llm_int["final_response"]["content"] == "Here is the answer" + assert llm_int["actual_usage"]["prompt_tokens"] == 100 + assert llm_int["attempts"][0]["status"] == "success" + + def test_update_llm_interaction_end_with_error(self, turn_manager, context_with_llm_turn): + """Test updating LLM interaction with error.""" + llm_response = { + "content": None, + "error": "Rate limit exceeded", + } + + turn_manager.update_llm_interaction_end(context_with_llm_turn, llm_response) + + llm_int = context_with_llm_turn["refs"]["team"]["turns"][0]["llm_interaction"] + assert llm_int["attempts"][0]["status"] == "failed" + assert llm_int["attempts"][0]["error"] == "Rate limit exceeded" + + +class TestTurnFinalization: + """Tests for turn finalization methods.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + @pytest.fixture + def context_with_running_turn(self): + """Context with a running turn.""" + turn = { + "turn_id": "turn-running", + "status": "running", + "end_time": None, + "llm_interaction": {"status": "running", "attempts": []}, + } + return { + "state": {"current_turn_id": "turn-running"}, + "refs": {"team": {"turns": [turn]}}, + } + + def test_finalize_current_turn(self, turn_manager, context_with_running_turn): + """Test finalize_current_turn sets status to completed.""" + turn_manager.finalize_current_turn(context_with_running_turn) + + turn = context_with_running_turn["refs"]["team"]["turns"][0] + assert turn["status"] == "completed" + assert turn["end_time"] is not None + + def test_finalize_current_turn_passes_baton(self, turn_manager, context_with_running_turn): + """Test finalize_current_turn updates last_turn_id.""" + turn_manager.finalize_current_turn(context_with_running_turn) + + assert context_with_running_turn["state"]["last_turn_id"] == "turn-running" + + def test_finalize_current_turn_with_next_action(self, turn_manager, context_with_running_turn): + """Test finalize_current_turn records next_action in outputs.""" + turn_manager.finalize_current_turn(context_with_running_turn, next_action="continue") + + turn = context_with_running_turn["refs"]["team"]["turns"][0] + assert turn["outputs"]["next_action"] == "continue" + + def test_fail_current_turn(self, turn_manager, context_with_running_turn): + """Test fail_current_turn marks turn as error.""" + turn_manager.fail_current_turn(context_with_running_turn, "Unexpected error") + + turn = context_with_running_turn["refs"]["team"]["turns"][0] + assert turn["status"] == "error" + assert turn["error_details"] == "Unexpected error" + assert turn["end_time"] is not None + + def test_fail_current_turn_updates_llm_interaction(self, turn_manager, context_with_running_turn): + """Test fail_current_turn also fails LLM interaction.""" + context_with_running_turn["refs"]["team"]["turns"][0]["llm_interaction"]["attempts"] = [ + {"stream_id": "s1", "status": "pending", "error": None} + ] + + turn_manager.fail_current_turn(context_with_running_turn, "LLM failed") + + llm_int = context_with_running_turn["refs"]["team"]["turns"][0]["llm_interaction"] + assert llm_int["status"] == "error" + assert llm_int["attempts"][0]["status"] == "failed" + + def test_cancel_current_turn(self, turn_manager, context_with_running_turn): + """Test cancel_current_turn marks turn as cancelled.""" + turn_manager.cancel_current_turn(context_with_running_turn) + + turn = context_with_running_turn["refs"]["team"]["turns"][0] + assert turn["status"] == "cancelled" + + +class TestSpecialTurns: + """Tests for special turn creation (delimiter, aggregation).""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + def test_create_restart_delimiter_turn(self, turn_manager): + """Test creating a restart delimiter turn.""" + team_state = {"turns": []} + + delimiter_id = turn_manager.create_restart_delimiter_turn( + team_state, "run-1", "flow-old", "last-turn-id" + ) + + assert delimiter_id.startswith("delimiter_") + assert len(team_state["turns"]) == 1 + + turn = team_state["turns"][0] + assert turn["turn_type"] == "restart_delimiter_turn" + assert turn["flow_id"] == "flow-old" + assert turn["source_turn_ids"] == ["last-turn-id"] + assert turn["status"] == "completed" + + def test_create_aggregation_turn(self, turn_manager): + """Test creating an aggregation turn.""" + team_state = {"turns": []} + dispatch_turn = { + "flow_id": "flow-main", + "agent_info": {"agent_id": "dispatcher"}, + } + + agg_id = turn_manager.create_aggregation_turn( + team_state, + run_id="run-1", + dispatch_turn=dispatch_turn, + last_turn_ids_of_subflows=["sub-1", "sub-2"], + dispatch_tool_call_id="dispatch-call-1", + aggregation_summary="Both tasks completed", + ) + + assert agg_id == "agg_dispatch-call-1" + + turn = team_state["turns"][0] + assert turn["turn_type"] == "aggregation_turn" + assert turn["source_turn_ids"] == ["sub-1", "sub-2"] + assert turn["outputs"]["aggregated_results_summary"] == "Both tasks completed" + + +class TestEnrichTurnInputs: + """Tests for enrich_turn_inputs method.""" + + @pytest.fixture + def turn_manager(self): + return TurnManager() + + @pytest.fixture + def context_for_enrichment(self): + """Context with turn ready for enrichment.""" + turn = { + "turn_id": "turn-enrich", + "source_tool_call_id": None, + "inputs": {"processed_inbox_items": []}, + "llm_interaction": {"predicted_usage": None}, + } + return { + "state": {"current_turn_id": "turn-enrich"}, + "refs": {"team": {"turns": [turn]}}, + "meta": {"agent_id": "test-agent"}, + } + + def test_enrich_turn_inputs_populates_inbox_items(self, turn_manager, context_for_enrichment): + """Test that inbox processing log is populated.""" + processing_result = { + "processing_log": [ + {"item_id": "inbox-1", "source": "USER_INPUT"}, + {"item_id": "inbox-2", "source": "DIRECTIVE"}, + ] + } + llm_call_package = {"predicted_total_tokens": 500} + system_prompt_details = {"construction_log": [], "final_prompt": "System prompt"} + + turn_manager.enrich_turn_inputs( + context_for_enrichment, "turn-enrich", + processing_result, llm_call_package, system_prompt_details + ) + + turn = context_for_enrichment["refs"]["team"]["turns"][0] + assert len(turn["inputs"]["processed_inbox_items"]) == 2 + + def test_enrich_turn_inputs_sets_source_tool_call_id(self, turn_manager, context_for_enrichment): + """Test that source_tool_call_id is set from TOOL_RESULT inbox item.""" + processing_result = { + "processing_log": [ + {"source": "USER_INPUT"}, + {"source": "TOOL_RESULT", "payload": {"tool_call_id": "tool-call-xyz"}}, + ] + } + + turn_manager.enrich_turn_inputs( + context_for_enrichment, "turn-enrich", + processing_result, {}, {"construction_log": [], "final_prompt": ""} + ) + + turn = context_for_enrichment["refs"]["team"]["turns"][0] + assert turn["source_tool_call_id"] == "tool-call-xyz" + + def test_enrich_turn_inputs_sets_predicted_usage(self, turn_manager, context_for_enrichment): + """Test that predicted token usage is set.""" + llm_call_package = {"predicted_total_tokens": 1500} + + turn_manager.enrich_turn_inputs( + context_for_enrichment, "turn-enrich", + {"processing_log": []}, llm_call_package, + {"construction_log": [], "final_prompt": ""} + ) + + turn = context_for_enrichment["refs"]["team"]["turns"][0] + assert turn["llm_interaction"]["predicted_usage"]["prompt_tokens"] == 1500 diff --git a/core/uv.lock b/core/uv.lock index 24b18f1..a11cfad 100644 --- a/core/uv.lock +++ b/core/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" resolution-markers = [ "python_version < '0'", @@ -11,18 +11,18 @@ resolution-markers = [ 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, upload_time = "2024-06-24T11:02:03.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload_time = "2024-06-24T11:02:01.529Z" }, + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, ] [[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, upload_time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload_time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] @@ -38,42 +38,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload_time = "2025-06-14T15:15:41.354Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload_time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload_time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload_time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload_time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload_time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload_time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload_time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload_time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload_time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload_time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload_time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload_time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload_time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload_time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload_time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload_time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload_time = "2025-06-14T15:14:29.05Z" }, - { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload_time = "2025-06-14T15:14:30.604Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload_time = "2025-06-14T15:14:32.275Z" }, - { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload_time = "2025-06-14T15:14:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload_time = "2025-06-14T15:14:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload_time = "2025-06-14T15:14:38Z" }, - { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload_time = "2025-06-14T15:14:39.951Z" }, - { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload_time = "2025-06-14T15:14:42.151Z" }, - { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload_time = "2025-06-14T15:14:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload_time = "2025-06-14T15:14:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload_time = "2025-06-14T15:14:47.911Z" }, - { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload_time = "2025-06-14T15:14:50.334Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload_time = "2025-06-14T15:14:52.378Z" }, - { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload_time = "2025-06-14T15:14:54.617Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload_time = "2025-06-14T15:14:56.597Z" }, - { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload_time = "2025-06-14T15:14:58.598Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload_time = "2025-06-14T15:15:00.939Z" }, - { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload_time = "2025-06-14T15:15:02.858Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, + { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, + { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload-time = "2025-06-14T15:14:30.604Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload-time = "2025-06-14T15:14:32.275Z" }, + { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload-time = "2025-06-14T15:14:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload-time = "2025-06-14T15:14:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload-time = "2025-06-14T15:14:38Z" }, + { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload-time = "2025-06-14T15:14:39.951Z" }, + { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload-time = "2025-06-14T15:14:42.151Z" }, + { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload-time = "2025-06-14T15:14:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload-time = "2025-06-14T15:14:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload-time = "2025-06-14T15:14:47.911Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload-time = "2025-06-14T15:14:50.334Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload-time = "2025-06-14T15:14:52.378Z" }, + { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload-time = "2025-06-14T15:14:54.617Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload-time = "2025-06-14T15:14:56.597Z" }, + { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload-time = "2025-06-14T15:14:58.598Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload-time = "2025-06-14T15:15:00.939Z" }, + { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload-time = "2025-06-14T15:15:02.858Z" }, ] [[package]] @@ -84,9 +84,9 @@ dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload_time = "2025-07-03T22:54:43.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload_time = "2025-07-03T22:54:42.156Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] @@ -96,18 +96,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload_time = "2025-02-03T07:30:16.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload_time = "2025-02-03T07:30:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, ] [[package]] @@ -119,58 +138,58 @@ dependencies = [ { 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, upload_time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[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, upload_time = "2025-03-13T11:10:22.779Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[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, upload_time = "2024-08-04T21:14:43.957Z" } -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, upload_time = "2024-08-04T21:13:56.209Z" }, - { 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, upload_time = "2024-08-04T21:13:59.966Z" }, - { 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, upload_time = "2024-08-04T21:14:00.846Z" }, - { 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, upload_time = "2024-08-04T21:14:01.989Z" }, - { 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, upload_time = "2024-08-04T21:14:03.509Z" }, - { 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, upload_time = "2024-08-04T21:14:04.679Z" }, - { 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, upload_time = "2024-08-04T21:14:09.038Z" }, - { 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, upload_time = "2024-08-04T21:14:09.99Z" }, - { 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, upload_time = "2024-08-04T21:14:11.468Z" }, - { 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, upload_time = "2024-08-04T21:14:12.394Z" }, - { 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, upload_time = "2024-08-04T21:14:13.707Z" }, - { 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, upload_time = "2024-08-04T21:14:14.74Z" }, - { 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, upload_time = "2024-08-04T21:14:19.155Z" }, - { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload_time = "2024-08-04T21:14:20.438Z" }, - { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload_time = "2024-08-04T21:14:21.342Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload_time = "2024-08-04T21:14:22.193Z" }, - { 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, upload_time = "2024-08-04T21:14:23.034Z" }, - { 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, upload_time = "2024-08-04T21:14:23.922Z" }, - { 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, upload_time = "2024-08-04T21:14:28.061Z" }, - { 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, upload_time = "2024-08-04T21:14:29.586Z" }, - { 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, upload_time = "2024-08-04T21:14:30.481Z" }, - { 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, upload_time = "2024-08-04T21:14:31.883Z" }, - { 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, upload_time = "2024-08-04T21:14:32.751Z" }, - { 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, upload_time = "2024-08-04T21:14:34.147Z" }, - { 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, upload_time = "2024-08-04T21:14:35.276Z" }, - { 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, upload_time = "2024-08-04T21:14:36.158Z" }, - { 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, upload_time = "2024-08-04T21:14:37.185Z" }, - { 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, upload_time = "2024-08-04T21:14:38.145Z" }, - { 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, upload_time = "2024-08-04T21:14:39.128Z" }, - { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482, upload_time = "2024-08-04T21:14:40.269Z" }, - { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780, upload_time = "2024-08-04T21:14:41.128Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload_time = "2024-08-04T21:14:42.803Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/dd/3b/69ff8a885e4c1c42014c2765275c4bd91fe7bc9847e9d8543dbcbb09f820/audioop_lts-0.2.1.tar.gz", hash = "sha256:e81268da0baa880431b68b1308ab7257eb33f356e57a5f9b1f915dfb13dd1387", size = 30204, upload-time = "2024-08-04T21:14:43.957Z" } +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, upload-time = "2024-08-04T21:13:56.209Z" }, + { 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, upload-time = "2024-08-04T21:13:59.966Z" }, + { 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, upload-time = "2024-08-04T21:14:00.846Z" }, + { 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, upload-time = "2024-08-04T21:14:01.989Z" }, + { 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, upload-time = "2024-08-04T21:14:03.509Z" }, + { 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, upload-time = "2024-08-04T21:14:04.679Z" }, + { 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, upload-time = "2024-08-04T21:14:09.038Z" }, + { 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, upload-time = "2024-08-04T21:14:09.99Z" }, + { 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, upload-time = "2024-08-04T21:14:11.468Z" }, + { 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, upload-time = "2024-08-04T21:14:12.394Z" }, + { 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, upload-time = "2024-08-04T21:14:13.707Z" }, + { 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, upload-time = "2024-08-04T21:14:14.74Z" }, + { 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, upload-time = "2024-08-04T21:14:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/ef/9a/4699b0c4fcf89936d2bfb5425f55f1a8b86dff4237cfcc104946c9cd9858/audioop_lts-0.2.1-cp313-abi3-win32.whl", hash = "sha256:6e899eb8874dc2413b11926b5fb3857ec0ab55222840e38016a6ba2ea9b7d5e3", size = 26059, upload-time = "2024-08-04T21:14:20.438Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1c/1f88e9c5dd4785a547ce5fd1eb83fff832c00cc0e15c04c1119b02582d06/audioop_lts-0.2.1-cp313-abi3-win_amd64.whl", hash = "sha256:64562c5c771fb0a8b6262829b9b4f37a7b886c01b4d3ecdbae1d629717db08b4", size = 30412, upload-time = "2024-08-04T21:14:21.342Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/c123fd29d89a6402ad261516f848437472ccc602abb59bba522af45e281b/audioop_lts-0.2.1-cp313-abi3-win_arm64.whl", hash = "sha256:c45317debeb64002e980077642afbd977773a25fa3dfd7ed0c84dccfc1fafcb0", size = 23578, upload-time = "2024-08-04T21:14:22.193Z" }, + { 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, upload-time = "2024-08-04T21:14:23.034Z" }, + { 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, upload-time = "2024-08-04T21:14:23.922Z" }, + { 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, upload-time = "2024-08-04T21:14:28.061Z" }, + { 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, upload-time = "2024-08-04T21:14:29.586Z" }, + { 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, upload-time = "2024-08-04T21:14:30.481Z" }, + { 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, upload-time = "2024-08-04T21:14:31.883Z" }, + { 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, upload-time = "2024-08-04T21:14:32.751Z" }, + { 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, upload-time = "2024-08-04T21:14:34.147Z" }, + { 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, upload-time = "2024-08-04T21:14:35.276Z" }, + { 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, upload-time = "2024-08-04T21:14:36.158Z" }, + { 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, upload-time = "2024-08-04T21:14:37.185Z" }, + { 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, upload-time = "2024-08-04T21:14:38.145Z" }, + { 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, upload-time = "2024-08-04T21:14:39.128Z" }, + { url = "https://files.pythonhosted.org/packages/65/6d/42d40da100be1afb661fd77c2b1c0dfab08af1540df57533621aea3db52a/audioop_lts-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:f51bb55122a89f7a0817d7ac2319744b4640b5b446c4c3efcea5764ea99ae509", size = 26482, upload-time = "2024-08-04T21:14:40.269Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/f08494dca79f65212f5b273aecc5a2f96691bf3307cac29acfcf84300c01/audioop_lts-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f0f2f336aa2aee2bce0b0dcc32bbba9178995454c7b979cf6ce086a8801e14c7", size = 30780, upload-time = "2024-08-04T21:14:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/be73b6015511aa0173ec595fc579133b797ad532996f2998fd6b8d1bbe6b/audioop_lts-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:78bfb3703388c780edf900be66e07de5a3d4105ca8e8720c5c4d67927e0b15d0", size = 23918, upload-time = "2024-08-04T21:14:42.803Z" }, ] [[package]] @@ -180,9 +199,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371, upload_time = "2025-05-23T00:21:45.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/9d/b1e08d36899c12c8b894a44a5583ee157789f26fc4b176f8e4b6217b56e1/authlib-1.6.0.tar.gz", hash = "sha256:4367d32031b7af175ad3a323d571dc7257b7099d55978087ceae4a0d88cd3210", size = 158371, upload-time = "2025-05-23T00:21:45.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload_time = "2025-05-23T00:21:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/587c189bbab1ccc8c86a03a5d0e13873df916380ef1be461ebe6acebf48d/authlib-1.6.0-py2.py3-none-any.whl", hash = "sha256:91685589498f79e8655e8a8947431ad6288831d643f11c55c2143ffcc738048d", size = 239981, upload-time = "2025-05-23T00:21:43.075Z" }, ] [[package]] @@ -194,9 +213,9 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload_time = "2025-03-27T02:46:20.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940, upload-time = "2025-03-27T02:46:20.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload_time = "2025-03-27T02:46:22.356Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005, upload-time = "2025-03-27T02:46:22.356Z" }, ] [[package]] @@ -208,9 +227,9 @@ dependencies = [ { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload_time = "2025-07-03T00:55:23.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload_time = "2025-07-03T00:55:25.238Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, ] [[package]] @@ -224,9 +243,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload_time = "2025-05-14T00:18:30.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload-time = "2025-05-14T00:18:30.408Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload_time = "2025-05-14T00:18:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload-time = "2025-05-14T00:18:32.734Z" }, ] [[package]] @@ -237,18 +256,32 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "build" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, ] [[package]] name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload_time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload_time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] [[package]] @@ -259,18 +292,18 @@ dependencies = [ { name = "attrs" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016, upload_time = "2025-06-04T20:27:15.44Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/2b/561d78f488dcc303da4639e02021311728fb7fda8006dd2835550cddd9ed/cattrs-25.1.1.tar.gz", hash = "sha256:c914b734e0f2d59e5b720d145ee010f1fd9a13ee93900922a2f3f9d593b8382c", size = 435016, upload-time = "2025-06-04T20:27:15.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386, upload_time = "2025-06-04T20:27:13.969Z" }, + { url = "https://files.pythonhosted.org/packages/18/b0/215274ef0d835bbc1056392a367646648b6084e39d489099959aefcca2af/cattrs-25.1.1-py3-none-any.whl", hash = "sha256:1b40b2d3402af7be79a7e7e097a9b4cd16d4c06e6d526644b0b26a063a1cc064", size = 69386, upload-time = "2025-06-04T20:27:13.969Z" }, ] [[package]] name = "certifi" version = "2025.6.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload_time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload_time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, ] [[package]] @@ -280,65 +313,65 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload_time = "2024-09-04T20:45:21.852Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload_time = "2024-09-04T20:44:12.232Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload_time = "2024-09-04T20:44:13.739Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload_time = "2024-09-04T20:44:15.231Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload_time = "2024-09-04T20:44:17.188Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload_time = "2024-09-04T20:44:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload_time = "2024-09-04T20:44:20.248Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload_time = "2024-09-04T20:44:21.673Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload_time = "2024-09-04T20:44:23.245Z" }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload_time = "2024-09-04T20:44:24.757Z" }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload_time = "2024-09-04T20:44:26.208Z" }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload_time = "2024-09-04T20:44:27.578Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload_time = "2024-09-04T20:44:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload_time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload_time = "2024-09-04T20:44:32.01Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload_time = "2024-09-04T20:44:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload_time = "2024-09-04T20:44:35.191Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload_time = "2024-09-04T20:44:36.743Z" }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload_time = "2024-09-04T20:44:38.492Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload_time = "2024-09-04T20:44:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload_time = "2024-09-04T20:44:41.616Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload_time = "2024-09-04T20:44:43.733Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] [[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, upload_time = "2025-05-02T08:34:42.01Z" } -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, upload_time = "2025-05-02T08:32:33.712Z" }, - { 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, upload_time = "2025-05-02T08:32:35.768Z" }, - { 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, upload_time = "2025-05-02T08:32:37.284Z" }, - { 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, upload_time = "2025-05-02T08:32:38.803Z" }, - { 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, upload_time = "2025-05-02T08:32:40.251Z" }, - { 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, upload_time = "2025-05-02T08:32:41.705Z" }, - { 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, upload_time = "2025-05-02T08:32:43.709Z" }, - { 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, upload_time = "2025-05-02T08:32:46.197Z" }, - { 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, upload_time = "2025-05-02T08:32:48.105Z" }, - { 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, upload_time = "2025-05-02T08:32:49.719Z" }, - { 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, upload_time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload_time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload_time = "2025-05-02T08:32:54.573Z" }, - { 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, upload_time = "2025-05-02T08:32:56.363Z" }, - { 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, upload_time = "2025-05-02T08:32:58.551Z" }, - { 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, upload_time = "2025-05-02T08:33:00.342Z" }, - { 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, upload_time = "2025-05-02T08:33:02.081Z" }, - { 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, upload_time = "2025-05-02T08:33:04.063Z" }, - { 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, upload_time = "2025-05-02T08:33:06.418Z" }, - { 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, upload_time = "2025-05-02T08:33:08.183Z" }, - { 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, upload_time = "2025-05-02T08:33:09.986Z" }, - { 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, upload_time = "2025-05-02T08:33:11.814Z" }, - { 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, upload_time = "2025-05-02T08:33:13.707Z" }, - { 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, upload_time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload_time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload_time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload_time = "2025-05-02T08:34:40.053Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +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, upload-time = "2025-05-02T08:32:33.712Z" }, + { 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, upload-time = "2025-05-02T08:32:35.768Z" }, + { 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, upload-time = "2025-05-02T08:32:37.284Z" }, + { 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, upload-time = "2025-05-02T08:32:38.803Z" }, + { 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, upload-time = "2025-05-02T08:32:40.251Z" }, + { 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, upload-time = "2025-05-02T08:32:41.705Z" }, + { 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, upload-time = "2025-05-02T08:32:43.709Z" }, + { 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, upload-time = "2025-05-02T08:32:46.197Z" }, + { 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, upload-time = "2025-05-02T08:32:48.105Z" }, + { 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, upload-time = "2025-05-02T08:32:49.719Z" }, + { 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, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { 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, upload-time = "2025-05-02T08:32:56.363Z" }, + { 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, upload-time = "2025-05-02T08:32:58.551Z" }, + { 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, upload-time = "2025-05-02T08:33:00.342Z" }, + { 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, upload-time = "2025-05-02T08:33:02.081Z" }, + { 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, upload-time = "2025-05-02T08:33:04.063Z" }, + { 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, upload-time = "2025-05-02T08:33:06.418Z" }, + { 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, upload-time = "2025-05-02T08:33:08.183Z" }, + { 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, upload-time = "2025-05-02T08:33:09.986Z" }, + { 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, upload-time = "2025-05-02T08:33:11.814Z" }, + { 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, upload-time = "2025-05-02T08:33:13.707Z" }, + { 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, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] @@ -348,9 +381,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nltk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/39/883774dadb46a8ea348ddbdc9dfdb9aaa1a104825e65ee9ebe9a375f46e0/cleantext-1.1.4.tar.gz", hash = "sha256:854003de912406d8d821623774b307dc6f0626fd9fac0bdc5d24864ee3f37578", size = 4242, upload_time = "2021-12-29T22:08:33.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/39/883774dadb46a8ea348ddbdc9dfdb9aaa1a104825e65ee9ebe9a375f46e0/cleantext-1.1.4.tar.gz", hash = "sha256:854003de912406d8d821623774b307dc6f0626fd9fac0bdc5d24864ee3f37578", size = 4242, upload-time = "2021-12-29T22:08:33.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/d0/bd954cf316c1d3a605a9bc29d2cf2bbd388b82d2626b60ab92e8d18457a3/cleantext-1.1.4-py3-none-any.whl", hash = "sha256:138a658a8084796793910c876140002435ffc7ce51a9abf28d2a6b059a7a4d13", size = 4869, upload_time = "2021-12-29T22:08:32.003Z" }, + { url = "https://files.pythonhosted.org/packages/df/d0/bd954cf316c1d3a605a9bc29d2cf2bbd388b82d2626b60ab92e8d18457a3/cleantext-1.1.4-py3-none-any.whl", hash = "sha256:138a658a8084796793910c876140002435ffc7ce51a9abf28d2a6b059a7a4d13", size = 4869, upload-time = "2021-12-29T22:08:32.003Z" }, ] [[package]] @@ -360,27 +393,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload_time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload_time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] name = "cobble" version = "0.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805, upload_time = "2024-06-01T18:11:09.528Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/7a/a507c709be2c96e1bb6102eb7b7f4026c5e5e223ef7d745a17d239e9d844/cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa", size = 3805, upload-time = "2024-06-01T18:11:09.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984, upload_time = "2024-06-01T18:11:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/3714a2f371985215c219c2a70953d38e3eed81ef165aed061d21de0e998b/cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44", size = 3984, upload-time = "2024-06-01T18:11:07.911Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -390,9 +423,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "humanfriendly" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload_time = "2021-06-11T10:22:45.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload_time = "2021-06-11T10:22:42.561Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] [[package]] @@ -403,6 +436,7 @@ dependencies = [ { name = "aiofiles" }, { name = "aiohttp" }, { name = "aiosqlite" }, + { name = "anthropic" }, { name = "beautifulsoup4" }, { name = "cleantext" }, { name = "colorama" }, @@ -442,11 +476,20 @@ dependencies = [ { name = "yt-dlp" }, ] +[package.optional-dependencies] +dev = [ + { name = "pip-tools" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + [package.metadata] requires-dist = [ { name = "aiofiles", specifier = ">=24.1.0" }, { name = "aiohttp", specifier = ">=3.8.0" }, { name = "aiosqlite", specifier = ">=0.21.0" }, + { name = "anthropic", specifier = ">=0.40.0" }, { name = "beautifulsoup4", specifier = ">=4.9.3" }, { name = "cleantext", specifier = ">=1.1.0" }, { name = "colorama", specifier = ">=0.4.6" }, @@ -470,10 +513,14 @@ requires-dist = [ { name = "numba", specifier = "==0.61.2" }, { name = "numpy", specifier = ">=2" }, { name = "openai", specifier = ">=1.0.0" }, + { name = "pip-tools", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pocketflow", specifier = ">=0.0.1" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "pygls", specifier = "==1.3.1" }, { name = "pymupdf", specifier = ">=1.23.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-json-logger", specifier = ">=2.0.7" }, { name = "pyyaml", specifier = ">=6.0" }, @@ -485,14 +532,89 @@ requires-dist = [ { name = "websockets", specifier = ">=12.0" }, { name = "yt-dlp", specifier = ">=2023.12.30" }, ] +provides-extras = ["dev"] [[package]] name = "coolname" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/c6/1eaa4495ff4640e80d9af64f540e427ba1596a20f735d4c4750fe0386d07/coolname-2.2.0.tar.gz", hash = "sha256:6c5d5731759104479e7ca195a9b64f7900ac5bead40183c09323c7d0be9e75c7", size = 59006, upload_time = "2023-01-09T14:50:41.724Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/b1/5745d7523d8ce53b87779f46ef6cf5c5c342997939c2fe967e607b944e43/coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8", size = 37849, upload_time = "2023-01-09T14:50:39.897Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c5/c6/1eaa4495ff4640e80d9af64f540e427ba1596a20f735d4c4750fe0386d07/coolname-2.2.0.tar.gz", hash = "sha256:6c5d5731759104479e7ca195a9b64f7900ac5bead40183c09323c7d0be9e75c7", size = 59006, upload-time = "2023-01-09T14:50:41.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b1/5745d7523d8ce53b87779f46ef6cf5c5c342997939c2fe967e607b944e43/coolname-2.2.0-py2.py3-none-any.whl", hash = "sha256:4d1563186cfaf71b394d5df4c744f8c41303b6846413645e31d31915cdeb13e8", size = 37849, upload-time = "2023-01-09T14:50:39.897Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/45/2c665ca77ec32ad67e25c77daf1cee28ee4558f3bc571cdbaf88a00b9f23/coverage-7.13.0.tar.gz", hash = "sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936", size = 820905, upload-time = "2025-12-08T13:14:38.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f1/2619559f17f31ba00fc40908efd1fbf1d0a5536eb75dc8341e7d660a08de/coverage-7.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf", size = 218274, upload-time = "2025-12-08T13:12:52.095Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/30d71ae5d6e949ff93b2a79a2c1b4822e00423116c5c6edfaeef37301396/coverage-7.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f", size = 218638, upload-time = "2025-12-08T13:12:53.418Z" }, + { url = "https://files.pythonhosted.org/packages/79/c2/fce80fc6ded8d77e53207489d6065d0fed75db8951457f9213776615e0f5/coverage-7.13.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb", size = 250129, upload-time = "2025-12-08T13:12:54.744Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b6/51b5d1eb6fcbb9a1d5d6984e26cbe09018475c2922d554fd724dd0f056ee/coverage-7.13.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621", size = 252885, upload-time = "2025-12-08T13:12:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/972a5affea41de798691ab15d023d3530f9f56a72e12e243f35031846ff7/coverage-7.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74", size = 253974, upload-time = "2025-12-08T13:12:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/8a/56/116513aee860b2c7968aa3506b0f59b22a959261d1dbf3aea7b4450a7520/coverage-7.13.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57", size = 250538, upload-time = "2025-12-08T13:12:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/d6/75/074476d64248fbadf16dfafbf93fdcede389ec821f74ca858d7c87d2a98c/coverage-7.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8", size = 251912, upload-time = "2025-12-08T13:13:00.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d2/aa4f8acd1f7c06024705c12609d8698c51b27e4d635d717cd1934c9668e2/coverage-7.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d", size = 250054, upload-time = "2025-12-08T13:13:01.892Z" }, + { url = "https://files.pythonhosted.org/packages/19/98/8df9e1af6a493b03694a1e8070e024e7d2cdc77adedc225a35e616d505de/coverage-7.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b", size = 249619, upload-time = "2025-12-08T13:13:03.236Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/f8679231f3353018ca66ef647fa6fe7b77e6bff7845be54ab84f86233363/coverage-7.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd", size = 251496, upload-time = "2025-12-08T13:13:04.511Z" }, + { url = "https://files.pythonhosted.org/packages/04/86/9cb406388034eaf3c606c22094edbbb82eea1fa9d20c0e9efadff20d0733/coverage-7.13.0-cp312-cp312-win32.whl", hash = "sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef", size = 220808, upload-time = "2025-12-08T13:13:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/1c/59/af483673df6455795daf5f447c2f81a3d2fcfc893a22b8ace983791f6f34/coverage-7.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae", size = 221616, upload-time = "2025-12-08T13:13:07.95Z" }, + { url = "https://files.pythonhosted.org/packages/64/b0/959d582572b30a6830398c60dd419c1965ca4b5fb38ac6b7093a0d50ca8d/coverage-7.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080", size = 220261, upload-time = "2025-12-08T13:13:09.581Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cc/bce226595eb3bf7d13ccffe154c3c487a22222d87ff018525ab4dd2e9542/coverage-7.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf", size = 218297, upload-time = "2025-12-08T13:13:10.977Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9f/73c4d34600aae03447dff3d7ad1d0ac649856bfb87d1ca7d681cfc913f9e/coverage-7.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a", size = 218673, upload-time = "2025-12-08T13:13:12.562Z" }, + { url = "https://files.pythonhosted.org/packages/63/ab/8fa097db361a1e8586535ae5073559e6229596b3489ec3ef2f5b38df8cb2/coverage-7.13.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74", size = 249652, upload-time = "2025-12-08T13:13:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/3a/9bfd4de2ff191feb37ef9465855ca56a6f2f30a3bca172e474130731ac3d/coverage-7.13.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6", size = 252251, upload-time = "2025-12-08T13:13:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/b5d8105f016e1b5874af0d7c67542da780ccd4a5f2244a433d3e20ceb1ad/coverage-7.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b", size = 253492, upload-time = "2025-12-08T13:13:16.849Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b8/0fad449981803cc47a4694768b99823fb23632150743f9c83af329bb6090/coverage-7.13.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232", size = 249850, upload-time = "2025-12-08T13:13:18.142Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e9/8d68337c3125014d918cf4327d5257553a710a2995a6a6de2ac77e5aa429/coverage-7.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971", size = 251633, upload-time = "2025-12-08T13:13:19.56Z" }, + { url = "https://files.pythonhosted.org/packages/55/14/d4112ab26b3a1bc4b3c1295d8452dcf399ed25be4cf649002fb3e64b2d93/coverage-7.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d", size = 249586, upload-time = "2025-12-08T13:13:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a9/22b0000186db663b0d82f86c2f1028099ae9ac202491685051e2a11a5218/coverage-7.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137", size = 249412, upload-time = "2025-12-08T13:13:22.22Z" }, + { url = "https://files.pythonhosted.org/packages/a1/2e/42d8e0d9e7527fba439acdc6ed24a2b97613b1dc85849b1dd935c2cffef0/coverage-7.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511", size = 251191, upload-time = "2025-12-08T13:13:23.899Z" }, + { url = "https://files.pythonhosted.org/packages/a4/af/8c7af92b1377fd8860536aadd58745119252aaaa71a5213e5a8e8007a9f5/coverage-7.13.0-cp313-cp313-win32.whl", hash = "sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1", size = 220829, upload-time = "2025-12-08T13:13:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/58/f9/725e8bf16f343d33cbe076c75dc8370262e194ff10072c0608b8e5cf33a3/coverage-7.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a", size = 221640, upload-time = "2025-12-08T13:13:26.836Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ff/e98311000aa6933cc79274e2b6b94a2fe0fe3434fca778eba82003675496/coverage-7.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6", size = 220269, upload-time = "2025-12-08T13:13:28.116Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/bbaa2e1275b300343ea865f7d424cc0a2e2a1df6925a070b2b2d5d765330/coverage-7.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a", size = 218990, upload-time = "2025-12-08T13:13:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/1d/82f0b3323b3d149d7672e7744c116e9c170f4957e0c42572f0366dbb4477/coverage-7.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8", size = 219340, upload-time = "2025-12-08T13:13:31.524Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/fe3fd4702a3832a255f4d43013eacb0ef5fc155a5960ea9269d8696db28b/coverage-7.13.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053", size = 260638, upload-time = "2025-12-08T13:13:32.965Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/63186cb000307f2b4da463f72af9b85d380236965574c78e7e27680a2593/coverage-7.13.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071", size = 262705, upload-time = "2025-12-08T13:13:34.378Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a1/c0dacef0cc865f2455d59eed3548573ce47ed603205ffd0735d1d78b5906/coverage-7.13.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e", size = 265125, upload-time = "2025-12-08T13:13:35.73Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/82b99223628b61300bd382c205795533bed021505eab6dd86e11fb5d7925/coverage-7.13.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493", size = 259844, upload-time = "2025-12-08T13:13:37.69Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2c/89b0291ae4e6cd59ef042708e1c438e2290f8c31959a20055d8768349ee2/coverage-7.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0", size = 262700, upload-time = "2025-12-08T13:13:39.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/a5f992efae1996245e796bae34ceb942b05db275e4b34222a9a40b9fbd3b/coverage-7.13.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e", size = 260321, upload-time = "2025-12-08T13:13:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/a29f5d98c64fedbe32e2ac3c227fbf78edc01cc7572eee17d61024d89889/coverage-7.13.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c", size = 259222, upload-time = "2025-12-08T13:13:43.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c3/940fe447aae302a6701ee51e53af7e08b86ff6eed7631e5740c157ee22b9/coverage-7.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e", size = 261411, upload-time = "2025-12-08T13:13:44.72Z" }, + { url = "https://files.pythonhosted.org/packages/eb/31/12a4aec689cb942a89129587860ed4d0fd522d5fda81237147fde554b8ae/coverage-7.13.0-cp313-cp313t-win32.whl", hash = "sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46", size = 221505, upload-time = "2025-12-08T13:13:46.332Z" }, + { url = "https://files.pythonhosted.org/packages/65/8c/3b5fe3259d863572d2b0827642c50c3855d26b3aefe80bdc9eba1f0af3b0/coverage-7.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39", size = 222569, upload-time = "2025-12-08T13:13:47.79Z" }, + { url = "https://files.pythonhosted.org/packages/b0/39/f71fa8316a96ac72fc3908839df651e8eccee650001a17f2c78cdb355624/coverage-7.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e", size = 220841, upload-time = "2025-12-08T13:13:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4b/9b54bedda55421449811dcd5263a2798a63f48896c24dfb92b0f1b0845bd/coverage-7.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256", size = 218343, upload-time = "2025-12-08T13:13:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/c3a1f34d4bba2e592c8979f924da4d3d4598b0df2392fbddb7761258e3dc/coverage-7.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a", size = 218672, upload-time = "2025-12-08T13:13:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/07/62/eec0659e47857698645ff4e6ad02e30186eb8afd65214fd43f02a76537cb/coverage-7.13.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9", size = 249715, upload-time = "2025-12-08T13:13:53.791Z" }, + { url = "https://files.pythonhosted.org/packages/23/2d/3c7ff8b2e0e634c1f58d095f071f52ed3c23ff25be524b0ccae8b71f99f8/coverage-7.13.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19", size = 252225, upload-time = "2025-12-08T13:13:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ac/fb03b469d20e9c9a81093575003f959cf91a4a517b783aab090e4538764b/coverage-7.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be", size = 253559, upload-time = "2025-12-08T13:13:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/29/62/14afa9e792383c66cc0a3b872a06ded6e4ed1079c7d35de274f11d27064e/coverage-7.13.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb", size = 249724, upload-time = "2025-12-08T13:13:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/31/b7/333f3dab2939070613696ab3ee91738950f0467778c6e5a5052e840646b7/coverage-7.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8", size = 251582, upload-time = "2025-12-08T13:14:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/81/cb/69162bda9381f39b2287265d7e29ee770f7c27c19f470164350a38318764/coverage-7.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b", size = 249538, upload-time = "2025-12-08T13:14:02.556Z" }, + { url = "https://files.pythonhosted.org/packages/e0/76/350387b56a30f4970abe32b90b2a434f87d29f8b7d4ae40d2e8a85aacfb3/coverage-7.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9", size = 249349, upload-time = "2025-12-08T13:14:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/86/0d/7f6c42b8d59f4c7e43ea3059f573c0dcfed98ba46eb43c68c69e52ae095c/coverage-7.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927", size = 251011, upload-time = "2025-12-08T13:14:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/4bb2dff379721bb0b5c649d5c5eaf438462cad824acf32eb1b7ca0c7078e/coverage-7.13.0-cp314-cp314-win32.whl", hash = "sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f", size = 221091, upload-time = "2025-12-08T13:14:07.127Z" }, + { url = "https://files.pythonhosted.org/packages/ba/44/c239da52f373ce379c194b0ee3bcc121020e397242b85f99e0afc8615066/coverage-7.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc", size = 221904, upload-time = "2025-12-08T13:14:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/89/1f/b9f04016d2a29c2e4a0307baefefad1a4ec5724946a2b3e482690486cade/coverage-7.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b", size = 220480, upload-time = "2025-12-08T13:14:10.958Z" }, + { url = "https://files.pythonhosted.org/packages/16/d4/364a1439766c8e8647860584171c36010ca3226e6e45b1753b1b249c5161/coverage-7.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28", size = 219074, upload-time = "2025-12-08T13:14:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/71ba8be63351e099911051b2089662c03d5671437a0ec2171823c8e03bec/coverage-7.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe", size = 219342, upload-time = "2025-12-08T13:14:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/5e/25/127d8ed03d7711a387d96f132589057213e3aef7475afdaa303412463f22/coverage-7.13.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657", size = 260713, upload-time = "2025-12-08T13:14:16.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/db/559fbb6def07d25b2243663b46ba9eb5a3c6586c0c6f4e62980a68f0ee1c/coverage-7.13.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff", size = 262825, upload-time = "2025-12-08T13:14:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/37/99/6ee5bf7eff884766edb43bd8736b5e1c5144d0fe47498c3779326fe75a35/coverage-7.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3", size = 265233, upload-time = "2025-12-08T13:14:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/92f18fe0356ea69e1f98f688ed80cec39f44e9f09a1f26a1bbf017cc67f2/coverage-7.13.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b", size = 259779, upload-time = "2025-12-08T13:14:22.367Z" }, + { url = "https://files.pythonhosted.org/packages/90/5d/b312a8b45b37a42ea7d27d7d3ff98ade3a6c892dd48d1d503e773503373f/coverage-7.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d", size = 262700, upload-time = "2025-12-08T13:14:24.309Z" }, + { url = "https://files.pythonhosted.org/packages/63/f8/b1d0de5c39351eb71c366f872376d09386640840a2e09b0d03973d791e20/coverage-7.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e", size = 260302, upload-time = "2025-12-08T13:14:26.068Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7c/d42f4435bc40c55558b3109a39e2d456cddcec37434f62a1f1230991667a/coverage-7.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940", size = 259136, upload-time = "2025-12-08T13:14:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d3/23413241dc04d47cfe19b9a65b32a2edd67ecd0b817400c2843ebc58c847/coverage-7.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2", size = 261467, upload-time = "2025-12-08T13:14:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/13/e6/6e063174500eee216b96272c0d1847bf215926786f85c2bd024cf4d02d2f/coverage-7.13.0-cp314-cp314t-win32.whl", hash = "sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7", size = 221875, upload-time = "2025-12-08T13:14:31.106Z" }, + { url = "https://files.pythonhosted.org/packages/3b/46/f4fb293e4cbe3620e3ac2a3e8fd566ed33affb5861a9b20e3dd6c1896cbc/coverage-7.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc", size = 222982, upload-time = "2025-12-08T13:14:33.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/5b3b9018215ed9733fbd1ae3b2ed75c5de62c3b55377a52cae732e1b7805/coverage-7.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a", size = 221016, upload-time = "2025-12-08T13:14:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/1968f32fb9a2604645827e11ff84a31e59d532e01995f904723b4f5328b3/coverage-7.13.0-py3-none-any.whl", hash = "sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904", size = 210068, upload-time = "2025-12-08T13:14:36.236Z" }, ] [[package]] @@ -502,32 +624,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload_time = "2025-07-02T13:06:25.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload_time = "2025-07-02T13:05:01.514Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload_time = "2025-07-02T13:05:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload_time = "2025-07-02T13:05:07.084Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload_time = "2025-07-02T13:05:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload_time = "2025-07-02T13:05:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload_time = "2025-07-02T13:05:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload_time = "2025-07-02T13:05:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload_time = "2025-07-02T13:05:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload_time = "2025-07-02T13:05:18.743Z" }, - { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload_time = "2025-07-02T13:05:21.382Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload_time = "2025-07-02T13:05:23.39Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload_time = "2025-07-02T13:05:25.202Z" }, - { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload_time = "2025-07-02T13:05:27.229Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload_time = "2025-07-02T13:05:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload_time = "2025-07-02T13:05:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload_time = "2025-07-02T13:05:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload_time = "2025-07-02T13:05:34.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload_time = "2025-07-02T13:05:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload_time = "2025-07-02T13:05:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload_time = "2025-07-02T13:05:41.398Z" }, - { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload_time = "2025-07-02T13:05:43.64Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload_time = "2025-07-02T13:05:46.045Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload_time = "2025-07-02T13:05:48.329Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload_time = "2025-07-02T13:05:50.811Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, ] [[package]] @@ -538,58 +660,67 @@ dependencies = [ { name = "marshmallow" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload_time = "2024-06-09T16:20:19.103Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload_time = "2024-06-09T16:20:16.715Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload_time = "2021-03-08T10:59:26.269Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload_time = "2021-03-08T10:59:24.45Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload_time = "2023-12-24T09:54:32.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload_time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[package]] name = "dnspython" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload_time = "2024-10-05T20:14:59.362Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload_time = "2024-10-05T20:14:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, ] [[package]] name = "duckdb" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/ab/d89a4dd14311d5a0081711bc66db3fad73f7645fa7eb3844c423d2fa0a17/duckdb-1.3.1.tar.gz", hash = "sha256:8e101990a879533b1d33f003df2eb2a3c4bc7bdf976bd7ef7c32342047935327", size = 11628075, upload_time = "2025-06-16T13:57:04.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/ab/d89a4dd14311d5a0081711bc66db3fad73f7645fa7eb3844c423d2fa0a17/duckdb-1.3.1.tar.gz", hash = "sha256:8e101990a879533b1d33f003df2eb2a3c4bc7bdf976bd7ef7c32342047935327", size = 11628075, upload-time = "2025-06-16T13:57:04.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/cf/c9a76a15195ec1566b04a23c182ce16b60d1f06c7cdfec1aa538c8e8e0ae/duckdb-1.3.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:73f389f9c713325a6994dd9e04a7fa23bd73e8387883f8086946a9d3a1dd70e1", size = 15529437, upload_time = "2025-06-16T13:56:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/d7/15/6cb79d988bedb19be6cfb654cd98b339cf4d06b7fc337f52c4051416b690/duckdb-1.3.1-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:87c99569274b453d8f9963e43fea74bc86901773fac945c1fe612c133a91e506", size = 32525563, upload_time = "2025-06-16T13:56:19.235Z" }, - { url = "https://files.pythonhosted.org/packages/14/7a/0acc37ec937a69a2fc325ab680cf68e7f1ed5d83b056dfade617502e40c2/duckdb-1.3.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:21da268355dfdf859b3d4db22180f7d5dd85a60517e077cb4158768cd5f0ee44", size = 17106064, upload_time = "2025-06-16T13:56:21.534Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a0/aef95020f5ada03e44eea0b23951b96cec45a85a0c42210639d5d5688603/duckdb-1.3.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77902954d15ba4aff92e82df700643b995c057f2d7d39af7ed226d8cceb9c2af", size = 19172380, upload_time = "2025-06-16T13:56:23.875Z" }, - { url = "https://files.pythonhosted.org/packages/9c/2a/3eae3acda60e178785835d6df85f3bf9ddab4362e9fd45d0fe4879973561/duckdb-1.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67b1a3c9e2c3474991da97edfec0a89f382fef698d7f64b2d8d09006eaeeea24", size = 21123030, upload_time = "2025-06-16T13:56:26.366Z" }, - { url = "https://files.pythonhosted.org/packages/f4/79/885c0ad2434fa7b353532580435d59bb007efb629740ba4eb273fc4c882c/duckdb-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f1d076b12f0d2a7f9090ad9e4057ac41af3e4785969e5997afd44922c7b141e0", size = 22774472, upload_time = "2025-06-16T13:56:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/d294613e4fccfc86f4718b2cede365a9a6313c938bf0547c78ec196a0b9c/duckdb-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:bf7d6884bfb67aef67aebb0bd2460ea1137c55b3fd8794a3530c653dbe0d4019", size = 11302743, upload_time = "2025-06-16T13:56:31.868Z" }, - { url = "https://files.pythonhosted.org/packages/d0/2e/5e1bf9f0b43bcb37dbe729d3a2c55da8b232137c15b0b63d2d51f96793b6/duckdb-1.3.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:72bbc8479c5d88e839a92c458c94c622f917ff0122853323728d6e25b0c3d4e1", size = 15529541, upload_time = "2025-06-16T13:56:34.011Z" }, - { url = "https://files.pythonhosted.org/packages/bc/ab/6b2e1efb133b2f4990710bd9a54e734a12a147eaead1102e36dd8d126494/duckdb-1.3.1-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:937de83df6bbe4bee5830ce80f568d4c0ebf3ef5eb809db3343d2161e4f6e42b", size = 32525596, upload_time = "2025-06-16T13:56:36.048Z" }, - { url = "https://files.pythonhosted.org/packages/68/9f/879f6f33a1d5b4afee9dd4082e97d9b43c21cf734c90164d10fd7303edb5/duckdb-1.3.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:21440dd37f073944badd495c299c6d085cd133633450467ec420c71897ac1d5b", size = 17106339, upload_time = "2025-06-16T13:56:38.358Z" }, - { url = "https://files.pythonhosted.org/packages/9a/06/5755f93be743ec27986f275847a85d44bb1bd6d8631492d337729fbe9145/duckdb-1.3.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:663610b591ea6964f140441c81b718e745704cf098c540e905b200b9079e2a5c", size = 19173540, upload_time = "2025-06-16T13:56:40.304Z" }, - { url = "https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8793b5abb365bbbf64ba3065f3a37951fe04f2d4506b0e24f3f8ecd08b3af4ba", size = 21122193, upload_time = "2025-06-16T13:56:42.321Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/b4576bbfa895a0ab125697fd58c0fbe54338672a9df25e7311bdf21f9e04/duckdb-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:27d775a5af405d1c228561830c8ccbe4e2832dafb4012f16c05fde1cde206dee", size = 22773434, upload_time = "2025-06-16T13:56:46.414Z" }, - { url = "https://files.pythonhosted.org/packages/94/b9/f5ae51f7331f79c184fd96456c0896de875149fdeb092084fd20433ec97c/duckdb-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:3eb045a9bf92da890d890cde2f676b3bda61b9de3b7dc46cbaaf75875b41e4b0", size = 11302770, upload_time = "2025-06-16T13:56:48.325Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cf/c9a76a15195ec1566b04a23c182ce16b60d1f06c7cdfec1aa538c8e8e0ae/duckdb-1.3.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:73f389f9c713325a6994dd9e04a7fa23bd73e8387883f8086946a9d3a1dd70e1", size = 15529437, upload-time = "2025-06-16T13:56:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/d7/15/6cb79d988bedb19be6cfb654cd98b339cf4d06b7fc337f52c4051416b690/duckdb-1.3.1-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:87c99569274b453d8f9963e43fea74bc86901773fac945c1fe612c133a91e506", size = 32525563, upload-time = "2025-06-16T13:56:19.235Z" }, + { url = "https://files.pythonhosted.org/packages/14/7a/0acc37ec937a69a2fc325ab680cf68e7f1ed5d83b056dfade617502e40c2/duckdb-1.3.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:21da268355dfdf859b3d4db22180f7d5dd85a60517e077cb4158768cd5f0ee44", size = 17106064, upload-time = "2025-06-16T13:56:21.534Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a0/aef95020f5ada03e44eea0b23951b96cec45a85a0c42210639d5d5688603/duckdb-1.3.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77902954d15ba4aff92e82df700643b995c057f2d7d39af7ed226d8cceb9c2af", size = 19172380, upload-time = "2025-06-16T13:56:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/3eae3acda60e178785835d6df85f3bf9ddab4362e9fd45d0fe4879973561/duckdb-1.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67b1a3c9e2c3474991da97edfec0a89f382fef698d7f64b2d8d09006eaeeea24", size = 21123030, upload-time = "2025-06-16T13:56:26.366Z" }, + { url = "https://files.pythonhosted.org/packages/f4/79/885c0ad2434fa7b353532580435d59bb007efb629740ba4eb273fc4c882c/duckdb-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f1d076b12f0d2a7f9090ad9e4057ac41af3e4785969e5997afd44922c7b141e0", size = 22774472, upload-time = "2025-06-16T13:56:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/d294613e4fccfc86f4718b2cede365a9a6313c938bf0547c78ec196a0b9c/duckdb-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:bf7d6884bfb67aef67aebb0bd2460ea1137c55b3fd8794a3530c653dbe0d4019", size = 11302743, upload-time = "2025-06-16T13:56:31.868Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2e/5e1bf9f0b43bcb37dbe729d3a2c55da8b232137c15b0b63d2d51f96793b6/duckdb-1.3.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:72bbc8479c5d88e839a92c458c94c622f917ff0122853323728d6e25b0c3d4e1", size = 15529541, upload-time = "2025-06-16T13:56:34.011Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ab/6b2e1efb133b2f4990710bd9a54e734a12a147eaead1102e36dd8d126494/duckdb-1.3.1-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:937de83df6bbe4bee5830ce80f568d4c0ebf3ef5eb809db3343d2161e4f6e42b", size = 32525596, upload-time = "2025-06-16T13:56:36.048Z" }, + { url = "https://files.pythonhosted.org/packages/68/9f/879f6f33a1d5b4afee9dd4082e97d9b43c21cf734c90164d10fd7303edb5/duckdb-1.3.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:21440dd37f073944badd495c299c6d085cd133633450467ec420c71897ac1d5b", size = 17106339, upload-time = "2025-06-16T13:56:38.358Z" }, + { url = "https://files.pythonhosted.org/packages/9a/06/5755f93be743ec27986f275847a85d44bb1bd6d8631492d337729fbe9145/duckdb-1.3.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:663610b591ea6964f140441c81b718e745704cf098c540e905b200b9079e2a5c", size = 19173540, upload-time = "2025-06-16T13:56:40.304Z" }, + { url = "https://files.pythonhosted.org/packages/90/a6/c8577b741974f106e24f8eb3efedc399be1a23cbbdcf49dd4bea5bb8aa4e/duckdb-1.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8793b5abb365bbbf64ba3065f3a37951fe04f2d4506b0e24f3f8ecd08b3af4ba", size = 21122193, upload-time = "2025-06-16T13:56:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/b4576bbfa895a0ab125697fd58c0fbe54338672a9df25e7311bdf21f9e04/duckdb-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:27d775a5af405d1c228561830c8ccbe4e2832dafb4012f16c05fde1cde206dee", size = 22773434, upload-time = "2025-06-16T13:56:46.414Z" }, + { url = "https://files.pythonhosted.org/packages/94/b9/f5ae51f7331f79c184fd96456c0896de875149fdeb092084fd20433ec97c/duckdb-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:3eb045a9bf92da890d890cde2f676b3bda61b9de3b7dc46cbaaf75875b41e4b0", size = 11302770, upload-time = "2025-06-16T13:56:48.325Z" }, ] [[package]] @@ -600,18 +731,18 @@ dependencies = [ { name = "dnspython" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload_time = "2024-06-20T11:30:30.034Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload-time = "2024-06-20T11:30:30.034Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload_time = "2024-06-20T11:30:28.248Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload-time = "2024-06-20T11:30:28.248Z" }, ] [[package]] name = "et-xmlfile" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload_time = "2024-10-25T17:25:40.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload_time = "2024-10-25T17:25:39.051Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] [[package]] @@ -621,9 +752,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload_time = "2025-05-10T17:42:51.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload_time = "2025-05-10T17:42:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] @@ -634,18 +765,17 @@ dependencies = [ { name = "numpy" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/9a/e33fc563f007924dd4ec3c5101fe5320298d6c13c158a24a9ed849058569/faiss_cpu-1.11.0.tar.gz", hash = "sha256:44877b896a2b30a61e35ea4970d008e8822545cb340eca4eff223ac7f40a1db9", size = 70218, upload_time = "2025-04-28T07:48:30.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/d3/7178fa07047fd770964a83543329bb5e3fc1447004cfd85186ccf65ec3ee/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:356437b9a46f98c25831cdae70ca484bd6c05065af6256d87f6505005e9135b9", size = 3313807, upload_time = "2025-04-28T07:47:54.533Z" }, - { url = "https://files.pythonhosted.org/packages/9e/71/25f5f7b70a9f22a3efe19e7288278da460b043a3b60ad98e4e47401ed5aa/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c4a3d35993e614847f3221c6931529c0bac637a00eff0d55293e1db5cb98c85f", size = 7913537, upload_time = "2025-04-28T07:47:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c8/a5cb8466c981ad47750e1d5fda3d4223c82f9da947538749a582b3a2d35c/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f9af33e0b8324e8199b93eb70ac4a951df02802a9dcff88e9afc183b11666f0", size = 3785180, upload_time = "2025-04-28T07:47:59.004Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eaf15a7d80e1aad74f56cf737b31b4547a1a664ad3c6e4cfaf90e82454a8/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48b7e7876829e6bdf7333041800fa3c1753bb0c47e07662e3ef55aca86981430", size = 31287630, upload_time = "2025-04-28T07:48:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5c/902a78347e9c47baaf133e47863134e564c39f9afe105795b16ee986b0df/faiss_cpu-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bdc199311266d2be9d299da52361cad981393327b2b8aa55af31a1b75eaaf522", size = 15005398, upload_time = "2025-04-28T07:48:04.232Z" }, - { url = "https://files.pythonhosted.org/packages/92/90/d2329ce56423cc61f4c20ae6b4db001c6f88f28bf5a7ef7f8bbc246fd485/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0c98e5feff83b87348e44eac4d578d6f201780dae6f27f08a11d55536a20b3a8", size = 3313807, upload_time = "2025-04-28T07:48:06.486Z" }, - { url = "https://files.pythonhosted.org/packages/24/14/8af8f996d54e6097a86e6048b1a2c958c52dc985eb4f935027615079939e/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:796e90389427b1c1fb06abdb0427bb343b6350f80112a2e6090ac8f176ff7416", size = 7913539, upload_time = "2025-04-28T07:48:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2b/437c2f36c3aa3cffe041479fced1c76420d3e92e1f434f1da3be3e6f32b1/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b6e355dda72b3050991bc32031b558b8f83a2b3537a2b9e905a84f28585b47e", size = 3785181, upload_time = "2025-04-28T07:48:10.594Z" }, - { url = "https://files.pythonhosted.org/packages/66/75/955527414371843f558234df66fa0b62c6e86e71e4022b1be9333ac6004c/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6c482d07194638c169b4422774366e7472877d09181ea86835e782e6304d4185", size = 31287635, upload_time = "2025-04-28T07:48:12.93Z" }, - { url = "https://files.pythonhosted.org/packages/50/51/35b7a3f47f7859363a367c344ae5d415ea9eda65db0a7d497c7ea2c0b576/faiss_cpu-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:13eac45299532b10e911bff1abbb19d1bf5211aa9e72afeade653c3f1e50e042", size = 15005455, upload_time = "2025-04-28T07:48:16.173Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/7178fa07047fd770964a83543329bb5e3fc1447004cfd85186ccf65ec3ee/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:356437b9a46f98c25831cdae70ca484bd6c05065af6256d87f6505005e9135b9", size = 3313807, upload-time = "2025-04-28T07:47:54.533Z" }, + { url = "https://files.pythonhosted.org/packages/9e/71/25f5f7b70a9f22a3efe19e7288278da460b043a3b60ad98e4e47401ed5aa/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c4a3d35993e614847f3221c6931529c0bac637a00eff0d55293e1db5cb98c85f", size = 7913537, upload-time = "2025-04-28T07:47:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c8/a5cb8466c981ad47750e1d5fda3d4223c82f9da947538749a582b3a2d35c/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f9af33e0b8324e8199b93eb70ac4a951df02802a9dcff88e9afc183b11666f0", size = 3785180, upload-time = "2025-04-28T07:47:59.004Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eaf15a7d80e1aad74f56cf737b31b4547a1a664ad3c6e4cfaf90e82454a8/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48b7e7876829e6bdf7333041800fa3c1753bb0c47e07662e3ef55aca86981430", size = 31287630, upload-time = "2025-04-28T07:48:01.248Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/902a78347e9c47baaf133e47863134e564c39f9afe105795b16ee986b0df/faiss_cpu-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bdc199311266d2be9d299da52361cad981393327b2b8aa55af31a1b75eaaf522", size = 15005398, upload-time = "2025-04-28T07:48:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/92/90/d2329ce56423cc61f4c20ae6b4db001c6f88f28bf5a7ef7f8bbc246fd485/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0c98e5feff83b87348e44eac4d578d6f201780dae6f27f08a11d55536a20b3a8", size = 3313807, upload-time = "2025-04-28T07:48:06.486Z" }, + { url = "https://files.pythonhosted.org/packages/24/14/8af8f996d54e6097a86e6048b1a2c958c52dc985eb4f935027615079939e/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:796e90389427b1c1fb06abdb0427bb343b6350f80112a2e6090ac8f176ff7416", size = 7913539, upload-time = "2025-04-28T07:48:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2b/437c2f36c3aa3cffe041479fced1c76420d3e92e1f434f1da3be3e6f32b1/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b6e355dda72b3050991bc32031b558b8f83a2b3537a2b9e905a84f28585b47e", size = 3785181, upload-time = "2025-04-28T07:48:10.594Z" }, + { url = "https://files.pythonhosted.org/packages/66/75/955527414371843f558234df66fa0b62c6e86e71e4022b1be9333ac6004c/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6c482d07194638c169b4422774366e7472877d09181ea86835e782e6304d4185", size = 31287635, upload-time = "2025-04-28T07:48:12.93Z" }, + { url = "https://files.pythonhosted.org/packages/50/51/35b7a3f47f7859363a367c344ae5d415ea9eda65db0a7d497c7ea2c0b576/faiss_cpu-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:13eac45299532b10e911bff1abbb19d1bf5211aa9e72afeade653c3f1e50e042", size = 15005455, upload-time = "2025-04-28T07:48:16.173Z" }, ] [[package]] @@ -657,9 +787,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload_time = "2025-06-26T15:29:08.21Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload_time = "2025-06-26T15:29:06.49Z" }, + { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, ] [[package]] @@ -677,96 +807,96 @@ dependencies = [ { name = "rich" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/1f/0031ea07bcad9f9b38d3500772d2749ca2b16335b92bd012f1d2f86a853e/fastmcp-2.10.1.tar.gz", hash = "sha256:450c72e523926a2203c7eecdb4a8b0507506667bc8736b8b7bb44f6312424649", size = 2730387, upload_time = "2025-07-02T04:57:24.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1f/0031ea07bcad9f9b38d3500772d2749ca2b16335b92bd012f1d2f86a853e/fastmcp-2.10.1.tar.gz", hash = "sha256:450c72e523926a2203c7eecdb4a8b0507506667bc8736b8b7bb44f6312424649", size = 2730387, upload-time = "2025-07-02T04:57:24.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/a2/52ef74287ec5fe0e5a0ffedde7d0809da5ec3ac85f4e3f2ed5587b39471a/fastmcp-2.10.1-py3-none-any.whl", hash = "sha256:17d0acea04eeb3464c9eca42b6774fb06b38b72cface9af6a7482b3aa561db13", size = 182108, upload_time = "2025-07-02T04:57:23.529Z" }, + { url = "https://files.pythonhosted.org/packages/29/a2/52ef74287ec5fe0e5a0ffedde7d0809da5ec3ac85f4e3f2ed5587b39471a/fastmcp-2.10.1-py3-none-any.whl", hash = "sha256:17d0acea04eeb3464c9eca42b6774fb06b38b72cface9af6a7482b3aa561db13", size = 182108, upload-time = "2025-07-02T04:57:23.529Z" }, ] [[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, upload_time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload_time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "flatbuffers" version = "25.2.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170, upload_time = "2025-02-11T04:26:46.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170, upload-time = "2025-02-11T04:26:46.257Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload_time = "2025-02-11T04:26:44.484Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload-time = "2025-02-11T04:26:44.484Z" }, ] [[package]] name = "frozenlist" version = "1.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload_time = "2025-06-09T23:02:35.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload_time = "2025-06-09T23:00:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload_time = "2025-06-09T23:00:43.481Z" }, - { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload_time = "2025-06-09T23:00:44.793Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload_time = "2025-06-09T23:00:46.125Z" }, - { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload_time = "2025-06-09T23:00:47.73Z" }, - { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload_time = "2025-06-09T23:00:49.742Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload_time = "2025-06-09T23:00:51.352Z" }, - { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload_time = "2025-06-09T23:00:52.855Z" }, - { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload_time = "2025-06-09T23:00:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload_time = "2025-06-09T23:00:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload_time = "2025-06-09T23:00:58.468Z" }, - { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload_time = "2025-06-09T23:01:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload_time = "2025-06-09T23:01:01.474Z" }, - { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload_time = "2025-06-09T23:01:02.961Z" }, - { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload_time = "2025-06-09T23:01:05.095Z" }, - { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload_time = "2025-06-09T23:01:06.54Z" }, - { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload_time = "2025-06-09T23:01:07.752Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload_time = "2025-06-09T23:01:09.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload_time = "2025-06-09T23:01:10.653Z" }, - { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload_time = "2025-06-09T23:01:12.296Z" }, - { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload_time = "2025-06-09T23:01:13.641Z" }, - { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload_time = "2025-06-09T23:01:15.264Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload_time = "2025-06-09T23:01:16.752Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload_time = "2025-06-09T23:01:18.202Z" }, - { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload_time = "2025-06-09T23:01:19.649Z" }, - { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload_time = "2025-06-09T23:01:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload_time = "2025-06-09T23:01:23.098Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload_time = "2025-06-09T23:01:24.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload_time = "2025-06-09T23:01:26.28Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload_time = "2025-06-09T23:01:27.887Z" }, - { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload_time = "2025-06-09T23:01:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload_time = "2025-06-09T23:01:31.287Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload_time = "2025-06-09T23:01:35.503Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload_time = "2025-06-09T23:01:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload_time = "2025-06-09T23:01:38.295Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload_time = "2025-06-09T23:01:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload_time = "2025-06-09T23:01:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload_time = "2025-06-09T23:01:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload_time = "2025-06-09T23:01:44.166Z" }, - { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload_time = "2025-06-09T23:01:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload_time = "2025-06-09T23:01:47.234Z" }, - { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload_time = "2025-06-09T23:01:48.819Z" }, - { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload_time = "2025-06-09T23:01:50.394Z" }, - { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload_time = "2025-06-09T23:01:52.234Z" }, - { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload_time = "2025-06-09T23:01:53.788Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload_time = "2025-06-09T23:01:55.769Z" }, - { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload_time = "2025-06-09T23:01:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload_time = "2025-06-09T23:01:58.936Z" }, - { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload_time = "2025-06-09T23:02:00.493Z" }, - { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload_time = "2025-06-09T23:02:02.072Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload_time = "2025-06-09T23:02:03.779Z" }, - { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload_time = "2025-06-09T23:02:34.204Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] [[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, upload_time = "2025-05-24T12:03:23.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload_time = "2025-05-24T12:03:21.66Z" }, + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, ] [[package]] @@ -778,9 +908,9 @@ dependencies = [ { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload_time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload_time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, ] [[package]] @@ -797,51 +927,51 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/37ac8cd4752e28e547b8a52765fe48a2ada2d0d286ea03f46e4d8c69ff4f/google_genai-1.24.0.tar.gz", hash = "sha256:bc896e30ad26d05a2af3d17c2ba10ea214a94f1c0cdb93d5c004dc038774e75a", size = 226740, upload_time = "2025-07-01T22:14:24.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/cf/37ac8cd4752e28e547b8a52765fe48a2ada2d0d286ea03f46e4d8c69ff4f/google_genai-1.24.0.tar.gz", hash = "sha256:bc896e30ad26d05a2af3d17c2ba10ea214a94f1c0cdb93d5c004dc038774e75a", size = 226740, upload-time = "2025-07-01T22:14:24.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/28/a35f64fc02e599808101617a21d447d241dadeba2aac1f4dc2d1179b8218/google_genai-1.24.0-py3-none-any.whl", hash = "sha256:98be8c51632576289ecc33cd84bcdaf4356ef0bef04ac7578660c49175af22b9", size = 226065, upload_time = "2025-07-01T22:14:23.177Z" }, + { url = "https://files.pythonhosted.org/packages/30/28/a35f64fc02e599808101617a21d447d241dadeba2aac1f4dc2d1179b8218/google_genai-1.24.0-py3-none-any.whl", hash = "sha256:98be8c51632576289ecc33cd84bcdaf4356ef0bef04ac7578660c49175af22b9", size = 226065, upload-time = "2025-07-01T22:14:23.177Z" }, ] [[package]] name = "greenlet" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload_time = "2025-06-05T16:16:09.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload_time = "2025-06-05T16:11:23.467Z" }, - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload_time = "2025-06-05T16:38:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload_time = "2025-06-05T16:41:36.343Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload_time = "2025-06-05T16:48:19.604Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload_time = "2025-06-05T16:13:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload_time = "2025-06-05T16:12:50.792Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload_time = "2025-06-05T16:36:48.59Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload_time = "2025-06-05T16:12:40.457Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload_time = "2025-06-05T16:29:49.244Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload_time = "2025-06-05T16:10:08.26Z" }, - { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload_time = "2025-06-05T16:38:53.983Z" }, - { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload_time = "2025-06-05T16:41:37.89Z" }, - { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload_time = "2025-06-05T16:48:21.467Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload_time = "2025-06-05T16:13:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload_time = "2025-06-05T16:12:51.91Z" }, - { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload_time = "2025-06-05T16:36:49.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload_time = "2025-06-05T16:12:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload_time = "2025-06-05T16:20:12.651Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload_time = "2025-06-05T16:10:47.525Z" }, - { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload_time = "2025-06-05T16:38:55.125Z" }, - { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload_time = "2025-06-05T16:41:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload_time = "2025-06-05T16:48:23.113Z" }, - { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload_time = "2025-06-05T16:13:07.972Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload_time = "2025-06-05T16:12:53.453Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload_time = "2025-06-05T16:15:20.111Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload_time = "2025-04-24T03:35:25.427Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload_time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -852,9 +982,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload_time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload_time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] @@ -867,18 +997,18 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "httpx-sse" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload_time = "2025-06-24T13:21:05.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload_time = "2025-06-24T13:21:04.772Z" }, + { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, ] [[package]] @@ -894,9 +1024,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/fd/5f81bae67096c5ab50d29a0230b8374f0245916cca192f8ee2fada51f4f6/huggingface_hub-0.25.2.tar.gz", hash = "sha256:a1014ea111a5f40ccd23f7f7ba8ac46e20fa3b658ced1f86a00c75c06ec6423c", size = 365806, upload_time = "2024-10-09T08:32:41.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/fd/5f81bae67096c5ab50d29a0230b8374f0245916cca192f8ee2fada51f4f6/huggingface_hub-0.25.2.tar.gz", hash = "sha256:a1014ea111a5f40ccd23f7f7ba8ac46e20fa3b658ced1f86a00c75c06ec6423c", size = 365806, upload-time = "2024-10-09T08:32:41.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/09/a535946bf2dc88e61341f39dc507530411bb3ea4eac493e5ec833e8f35bd/huggingface_hub-0.25.2-py3-none-any.whl", hash = "sha256:1897caf88ce7f97fe0110603d8f66ac264e3ba6accdf30cd66cc0fed5282ad25", size = 436575, upload_time = "2024-10-09T08:32:39.166Z" }, + { url = "https://files.pythonhosted.org/packages/64/09/a535946bf2dc88e61341f39dc507530411bb3ea4eac493e5ec833e8f35bd/huggingface_hub-0.25.2-py3-none-any.whl", hash = "sha256:1897caf88ce7f97fe0110603d8f66ac264e3ba6accdf30cd66cc0fed5282ad25", size = 436575, upload-time = "2024-10-09T08:32:39.166Z" }, ] [[package]] @@ -906,18 +1036,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyreadline3", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload_time = "2021-09-17T21:40:43.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload_time = "2021-09-17T21:40:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" }, ] [[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, upload_time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] @@ -927,18 +1057,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload_time = "2025-04-27T15:29:01.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload_time = "2025-04-27T15:29:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload_time = "2024-10-08T23:04:11.5Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload_time = "2024-10-08T23:04:09.501Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] @@ -948,75 +1087,75 @@ 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, upload_time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[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, upload_time = "2025-05-18T19:04:59.73Z" } -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, upload_time = "2025-05-18T19:03:44.637Z" }, - { 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, upload_time = "2025-05-18T19:03:46.341Z" }, - { 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, upload_time = "2025-05-18T19:03:47.596Z" }, - { 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, upload_time = "2025-05-18T19:03:49.334Z" }, - { 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, upload_time = "2025-05-18T19:03:50.66Z" }, - { 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, upload_time = "2025-05-18T19:03:51.98Z" }, - { 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, upload_time = "2025-05-18T19:03:53.703Z" }, - { 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, upload_time = "2025-05-18T19:03:55.046Z" }, - { 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, upload_time = "2025-05-18T19:03:56.386Z" }, - { 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, upload_time = "2025-05-18T19:03:57.675Z" }, - { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload_time = "2025-05-18T19:03:59.025Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload_time = "2025-05-18T19:04:00.305Z" }, - { 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, upload_time = "2025-05-18T19:04:02.078Z" }, - { 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, upload_time = "2025-05-18T19:04:03.347Z" }, - { 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, upload_time = "2025-05-18T19:04:04.709Z" }, - { 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, upload_time = "2025-05-18T19:04:06.912Z" }, - { 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, upload_time = "2025-05-18T19:04:08.222Z" }, - { 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, upload_time = "2025-05-18T19:04:09.566Z" }, - { 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, upload_time = "2025-05-18T19:04:10.98Z" }, - { 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, upload_time = "2025-05-18T19:04:12.722Z" }, - { 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, upload_time = "2025-05-18T19:04:14.261Z" }, - { 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, upload_time = "2025-05-18T19:04:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload_time = "2025-05-18T19:04:17.541Z" }, - { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload_time = "2025-05-18T19:04:19.21Z" }, - { 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, upload_time = "2025-05-18T19:04:20.583Z" }, - { 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, upload_time = "2025-05-18T19:04:22.363Z" }, - { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload_time = "2025-05-18T19:04:23.627Z" }, - { 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, upload_time = "2025-05-18T19:04:24.891Z" }, - { 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, upload_time = "2025-05-18T19:04:26.161Z" }, - { 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, upload_time = "2025-05-18T19:04:27.495Z" }, - { 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, upload_time = "2025-05-18T19:04:28.896Z" }, - { 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, upload_time = "2025-05-18T19:04:30.183Z" }, - { 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, upload_time = "2025-05-18T19:04:32.028Z" }, - { 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, upload_time = "2025-05-18T19:04:33.467Z" }, - { 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, upload_time = "2025-05-18T19:04:34.827Z" }, - { 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, upload_time = "2025-05-18T19:04:36.19Z" }, - { 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, upload_time = "2025-05-18T19:04:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload_time = "2025-05-18T19:04:38.837Z" }, - { 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, upload_time = "2025-05-18T19:04:40.612Z" }, - { 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, upload_time = "2025-05-18T19:04:41.894Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" } +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, upload-time = "2025-05-18T19:03:44.637Z" }, + { 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, upload-time = "2025-05-18T19:03:46.341Z" }, + { 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, upload-time = "2025-05-18T19:03:47.596Z" }, + { 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, upload-time = "2025-05-18T19:03:49.334Z" }, + { 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, upload-time = "2025-05-18T19:03:50.66Z" }, + { 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, upload-time = "2025-05-18T19:03:51.98Z" }, + { 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, upload-time = "2025-05-18T19:03:53.703Z" }, + { 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, upload-time = "2025-05-18T19:03:55.046Z" }, + { 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, upload-time = "2025-05-18T19:03:56.386Z" }, + { 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, upload-time = "2025-05-18T19:03:57.675Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" }, + { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" }, + { 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, upload-time = "2025-05-18T19:04:02.078Z" }, + { 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, upload-time = "2025-05-18T19:04:03.347Z" }, + { 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, upload-time = "2025-05-18T19:04:04.709Z" }, + { 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, upload-time = "2025-05-18T19:04:06.912Z" }, + { 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, upload-time = "2025-05-18T19:04:08.222Z" }, + { 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, upload-time = "2025-05-18T19:04:09.566Z" }, + { 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, upload-time = "2025-05-18T19:04:10.98Z" }, + { 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, upload-time = "2025-05-18T19:04:12.722Z" }, + { 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, upload-time = "2025-05-18T19:04:14.261Z" }, + { 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, upload-time = "2025-05-18T19:04:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" }, + { 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, upload-time = "2025-05-18T19:04:20.583Z" }, + { 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, upload-time = "2025-05-18T19:04:22.363Z" }, + { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" }, + { 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, upload-time = "2025-05-18T19:04:24.891Z" }, + { 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, upload-time = "2025-05-18T19:04:26.161Z" }, + { 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, upload-time = "2025-05-18T19:04:27.495Z" }, + { 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, upload-time = "2025-05-18T19:04:28.896Z" }, + { 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, upload-time = "2025-05-18T19:04:30.183Z" }, + { 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, upload-time = "2025-05-18T19:04:32.028Z" }, + { 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, upload-time = "2025-05-18T19:04:33.467Z" }, + { 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, upload-time = "2025-05-18T19:04:34.827Z" }, + { 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, upload-time = "2025-05-18T19:04:36.19Z" }, + { 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, upload-time = "2025-05-18T19:04:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" }, + { 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, upload-time = "2025-05-18T19:04:40.612Z" }, + { 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, upload-time = "2025-05-18T19:04:41.894Z" }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload_time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload_time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, ] [[package]] name = "json-repair" version = "0.40.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/36/e03fe9da84e04b475290f8612de7b229b78e37c80e44188b85fe56dbab66/json_repair-0.40.0.tar.gz", hash = "sha256:ce3cdef63f033d072295ca892cba51487292cd937da42dc20a8d629ecf5eb82d", size = 30098, upload_time = "2025-03-19T12:21:44.242Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/36/e03fe9da84e04b475290f8612de7b229b78e37c80e44188b85fe56dbab66/json_repair-0.40.0.tar.gz", hash = "sha256:ce3cdef63f033d072295ca892cba51487292cd937da42dc20a8d629ecf5eb82d", size = 30098, upload-time = "2025-03-19T12:21:44.242Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/e1/f0e63cc027669763ccc2c1e62ba69959ec02db5328c81df2508a52711ec9/json_repair-0.40.0-py3-none-any.whl", hash = "sha256:46955bfd22338ba60cc5239c0b01462ba419871b19fcd68d8881aca4fa3b0d2f", size = 20736, upload_time = "2025-03-19T12:21:42.867Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/f0e63cc027669763ccc2c1e62ba69959ec02db5328c81df2508a52711ec9/json_repair-0.40.0-py3-none-any.whl", hash = "sha256:46955bfd22338ba60cc5239c0b01462ba419871b19fcd68d8881aca4fa3b0d2f", size = 20736, upload-time = "2025-03-19T12:21:42.867Z" }, ] [[package]] @@ -1026,18 +1165,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpointer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload_time = "2023-06-26T12:07:29.144Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload_time = "2023-06-16T21:01:28.466Z" }, + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, ] [[package]] name = "jsonpointer" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload_time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload_time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, ] [[package]] @@ -1050,9 +1189,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload_time = "2025-05-26T18:48:10.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload_time = "2025-05-26T18:48:08.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, ] [[package]] @@ -1062,9 +1201,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload_time = "2025-04-23T12:34:07.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload_time = "2025-04-23T12:34:05.422Z" }, + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] @@ -1080,9 +1219,9 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/13/a9931800ee42bbe0f8850dd540de14e80dda4945e7ee36e20b5d5964286e/langchain-0.3.26.tar.gz", hash = "sha256:8ff034ee0556d3e45eff1f1e96d0d745ced57858414dba7171c8ebdbeb5580c9", size = 10226808, upload_time = "2025-06-20T22:23:01.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/a9931800ee42bbe0f8850dd540de14e80dda4945e7ee36e20b5d5964286e/langchain-0.3.26.tar.gz", hash = "sha256:8ff034ee0556d3e45eff1f1e96d0d745ced57858414dba7171c8ebdbeb5580c9", size = 10226808, upload-time = "2025-06-20T22:23:01.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/f2/c09a2e383283e3af1db669ab037ac05a45814f4b9c472c48dc24c0cef039/langchain-0.3.26-py3-none-any.whl", hash = "sha256:361bb2e61371024a8c473da9f9c55f4ee50f269c5ab43afdb2b1309cb7ac36cf", size = 1012336, upload_time = "2025-06-20T22:22:58.874Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f2/c09a2e383283e3af1db669ab037ac05a45814f4b9c472c48dc24c0cef039/langchain-0.3.26-py3-none-any.whl", hash = "sha256:361bb2e61371024a8c473da9f9c55f4ee50f269c5ab43afdb2b1309cb7ac36cf", size = 1012336, upload-time = "2025-06-20T22:22:58.874Z" }, ] [[package]] @@ -1103,9 +1242,9 @@ dependencies = [ { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/76/200494f6de488217a196c4369e665d26b94c8c3642d46e2fd62f9daf0a3a/langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882", size = 33237737, upload_time = "2025-07-02T18:47:02.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/76/200494f6de488217a196c4369e665d26b94c8c3642d46e2fd62f9daf0a3a/langchain_community-0.3.27.tar.gz", hash = "sha256:e1037c3b9da0c6d10bf06e838b034eb741e016515c79ef8f3f16e53ead33d882", size = 33237737, upload-time = "2025-07-02T18:47:02.329Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/bc/f8c7dae8321d37ed39ac9d7896617c4203248240a4835b136e3724b3bb62/langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d", size = 2530442, upload_time = "2025-07-02T18:47:00.246Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bc/f8c7dae8321d37ed39ac9d7896617c4203248240a4835b136e3724b3bb62/langchain_community-0.3.27-py3-none-any.whl", hash = "sha256:581f97b795f9633da738ea95da9cb78f8879b538090c9b7a68c0aed49c828f0d", size = 2530442, upload-time = "2025-07-02T18:47:00.246Z" }, ] [[package]] @@ -1121,9 +1260,9 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/20/f5b18a17bfbe3416177e702ab2fd230b7d168abb17be31fb48f43f0bb772/langchain_core-0.3.68.tar.gz", hash = "sha256:312e1932ac9aa2eaf111b70fdc171776fa571d1a86c1f873dcac88a094b19c6f", size = 563041, upload_time = "2025-07-03T17:02:28.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/20/f5b18a17bfbe3416177e702ab2fd230b7d168abb17be31fb48f43f0bb772/langchain_core-0.3.68.tar.gz", hash = "sha256:312e1932ac9aa2eaf111b70fdc171776fa571d1a86c1f873dcac88a094b19c6f", size = 563041, upload-time = "2025-07-03T17:02:28.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/da/c89be0a272993bfcb762b2a356b9f55de507784c2755ad63caec25d183bf/langchain_core-0.3.68-py3-none-any.whl", hash = "sha256:5e5c1fbef419590537c91b8c2d86af896fbcbaf0d5ed7fdcdd77f7d8f3467ba0", size = 441405, upload_time = "2025-07-03T17:02:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/c89be0a272993bfcb762b2a356b9f55de507784c2755ad63caec25d183bf/langchain_core-0.3.68-py3-none-any.whl", hash = "sha256:5e5c1fbef419590537c91b8c2d86af896fbcbaf0d5ed7fdcdd77f7d8f3467ba0", size = 441405, upload-time = "2025-07-03T17:02:27.115Z" }, ] [[package]] @@ -1135,9 +1274,9 @@ dependencies = [ { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/e65261a08a03dd43f0ef8a539930b56548ac8136e71258c220d3589d1d07/langchain_openai-0.3.27.tar.gz", hash = "sha256:5d5a55adbff739274dfc3a4102925771736f893758f63679b64ae62fed79ca30", size = 753326, upload_time = "2025-06-27T17:56:29.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/e65261a08a03dd43f0ef8a539930b56548ac8136e71258c220d3589d1d07/langchain_openai-0.3.27.tar.gz", hash = "sha256:5d5a55adbff739274dfc3a4102925771736f893758f63679b64ae62fed79ca30", size = 753326, upload-time = "2025-06-27T17:56:29.904Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/31/1f0baf6490b082bf4d06f355c5e9c28728931dbf321f3ca03137617a692e/langchain_openai-0.3.27-py3-none-any.whl", hash = "sha256:efe636c3523978c44adc41cf55c8b3766c05c77547982465884d1258afe705df", size = 70368, upload_time = "2025-06-27T17:56:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/31/1f0baf6490b082bf4d06f355c5e9c28728931dbf321f3ca03137617a692e/langchain_openai-0.3.27-py3-none-any.whl", hash = "sha256:efe636c3523978c44adc41cf55c8b3766c05c77547982465884d1258afe705df", size = 70368, upload-time = "2025-06-27T17:56:28.726Z" }, ] [[package]] @@ -1147,9 +1286,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/ac/b4a25c5716bb0103b1515f1f52cc69ffb1035a5a225ee5afe3aed28bf57b/langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e", size = 42128, upload_time = "2025-04-04T14:03:51.521Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ac/b4a25c5716bb0103b1515f1f52cc69ffb1035a5a225ee5afe3aed28bf57b/langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e", size = 42128, upload-time = "2025-04-04T14:03:51.521Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/a3/3696ff2444658053c01b6b7443e761f28bb71217d82bb89137a978c5f66f/langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02", size = 32440, upload_time = "2025-04-04T14:03:50.6Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a3/3696ff2444658053c01b6b7443e761f28bb71217d82bb89137a978c5f66f/langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02", size = 32440, upload-time = "2025-04-04T14:03:50.6Z" }, ] [[package]] @@ -1165,18 +1304,18 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload_time = "2025-06-27T19:20:36.207Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c8/8d2e0fc438d2d3d8d4300f7684ea30a754344ed00d7ba9cc2705241d2a5f/langsmith-0.4.4.tar.gz", hash = "sha256:70c53bbff24a7872e88e6fa0af98270f4986a6e364f9e85db1cc5636defa4d66", size = 352105, upload-time = "2025-06-27T19:20:36.207Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload_time = "2025-06-27T19:20:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/1d/33/a3337eb70d795495a299a1640d7a75f17fb917155a64309b96106e7b9452/langsmith-0.4.4-py3-none-any.whl", hash = "sha256:014c68329bd085bd6c770a6405c61bb6881f82eb554ce8c4d1984b0035fd1716", size = 367687, upload-time = "2025-06-27T19:20:33.839Z" }, ] [[package]] name = "lark" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload_time = "2024-08-13T19:49:00.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/60/bc7622aefb2aee1c0b4ba23c1446d3e30225c8770b38d7aedbfb65ca9d5a/lark-1.2.2.tar.gz", hash = "sha256:ca807d0162cd16cef15a8feecb862d7319e7a09bdb13aef927968e45040fed80", size = 252132, upload-time = "2024-08-13T19:49:00.652Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload_time = "2024-08-13T19:48:58.603Z" }, + { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036, upload-time = "2024-08-13T19:48:58.603Z" }, ] [[package]] @@ -1207,27 +1346,27 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/99/889207aa89037da4cb381201918e91b6ab74088d401f9a1b8cfd7b5a48e9/litellm-1.73.6.post1.tar.gz", hash = "sha256:cc221818f572fdd64d22b89fa35e6153bfb4f0f755b68eec7da3542767e6a934", size = 8898978, upload_time = "2025-07-04T00:34:43.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/99/889207aa89037da4cb381201918e91b6ab74088d401f9a1b8cfd7b5a48e9/litellm-1.73.6.post1.tar.gz", hash = "sha256:cc221818f572fdd64d22b89fa35e6153bfb4f0f755b68eec7da3542767e6a934", size = 8898978, upload-time = "2025-07-04T00:34:43.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/34/0068ea25da50072d4f36a78299395afa3b3ed1c475be925fe33f8296c977/litellm-1.73.6.post1-py3-none-any.whl", hash = "sha256:c7a9ac5be16e176a8005e9196446b5c0ead0f08436c12a80920734ce6fa604d4", size = 8467486, upload_time = "2025-07-04T00:34:40.685Z" }, + { url = "https://files.pythonhosted.org/packages/a6/34/0068ea25da50072d4f36a78299395afa3b3ed1c475be925fe33f8296c977/litellm-1.73.6.post1-py3-none-any.whl", hash = "sha256:c7a9ac5be16e176a8005e9196446b5c0ead0f08436c12a80920734ce6fa604d4", size = 8467486, upload-time = "2025-07-04T00:34:40.685Z" }, ] [[package]] name = "llvmlite" version = "0.44.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload_time = "2025-01-20T11:14:41.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload_time = "2025-01-20T11:13:32.57Z" }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload_time = "2025-01-20T11:13:38.744Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload_time = "2025-01-20T11:13:46.711Z" }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload_time = "2025-01-20T11:13:56.159Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload_time = "2025-01-20T11:14:02.442Z" }, - { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload_time = "2025-01-20T11:14:09.035Z" }, - { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload_time = "2025-01-20T11:14:15.401Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload_time = "2025-01-20T11:14:22.949Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload_time = "2025-01-20T11:14:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload_time = "2025-01-20T11:14:38.578Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" }, ] [[package]] @@ -1238,49 +1377,49 @@ dependencies = [ { name = "attrs" }, { name = "cattrs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434, upload_time = "2024-01-09T17:21:12.625Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/f6/6e80484ec078d0b50699ceb1833597b792a6c695f90c645fbaf54b947e6f/lsprotocol-2023.0.1.tar.gz", hash = "sha256:cc5c15130d2403c18b734304339e51242d3018a05c4f7d0f198ad6e0cd21861d", size = 69434, upload-time = "2024-01-09T17:21:12.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826, upload_time = "2024-01-09T17:21:14.491Z" }, + { url = "https://files.pythonhosted.org/packages/8d/37/2351e48cb3309673492d3a8c59d407b75fb6630e560eb27ecd4da03adc9a/lsprotocol-2023.0.1-py3-none-any.whl", hash = "sha256:c75223c9e4af2f24272b14c6375787438279369236cd568f596d4951052a60f2", size = 70826, upload-time = "2024-01-09T17:21:14.491Z" }, ] [[package]] name = "lxml" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload_time = "2025-06-26T16:28:19.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload_time = "2025-06-26T16:26:06.776Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload_time = "2025-06-26T16:26:09.511Z" }, - { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload_time = "2025-06-26T16:26:12.337Z" }, - { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload_time = "2025-06-28T18:47:25.602Z" }, - { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload_time = "2025-06-28T18:47:28.136Z" }, - { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload_time = "2025-06-26T16:26:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload_time = "2025-07-03T19:19:06.008Z" }, - { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload_time = "2025-06-26T16:26:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload_time = "2025-06-26T16:26:20.292Z" }, - { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload_time = "2025-06-26T16:26:22.765Z" }, - { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload_time = "2025-06-26T16:26:26.461Z" }, - { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload_time = "2025-07-03T19:19:09.837Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload_time = "2025-06-26T16:26:29.406Z" }, - { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload_time = "2025-06-26T16:26:31.588Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload_time = "2025-06-26T16:26:33.723Z" }, - { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload_time = "2025-06-26T16:26:35.959Z" }, - { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload_time = "2025-06-26T16:26:39.079Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload_time = "2025-06-26T16:26:41.891Z" }, - { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload_time = "2025-06-26T16:26:44.669Z" }, - { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload_time = "2025-06-28T18:47:31.091Z" }, - { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload_time = "2025-06-28T18:47:33.612Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload_time = "2025-06-26T16:26:47.503Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload_time = "2025-07-03T19:19:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload_time = "2025-06-26T16:26:49.998Z" }, - { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload_time = "2025-06-26T16:26:52.564Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload_time = "2025-06-26T16:26:55.054Z" }, - { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload_time = "2025-06-26T16:26:57.384Z" }, - { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload_time = "2025-07-03T19:19:16.409Z" }, - { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload_time = "2025-06-26T16:27:00.031Z" }, - { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload_time = "2025-06-26T16:27:04.251Z" }, - { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload_time = "2025-06-26T16:27:06.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload_time = "2025-06-26T16:27:09.888Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, ] [[package]] @@ -1293,12 +1432,12 @@ dependencies = [ { name = "onnxruntime" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634, upload_time = "2025-05-02T14:54:18.88Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/8fdd991142ad3e037179a494b153f463024e5a211ef3ad948b955c26b4de/magika-0.6.2.tar.gz", hash = "sha256:37eb6ae8020f6e68f231bc06052c0a0cbe8e6fa27492db345e8dc867dbceb067", size = 3036634, upload-time = "2025-05-02T14:54:18.88Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/07/4f7748f34279f2852068256992377474f9700b6fbad6735d6be58605178f/magika-0.6.2-py3-none-any.whl", hash = "sha256:5ef72fbc07723029b3684ef81454bc224ac5f60986aa0fc5a28f4456eebcb5b2", size = 2967609, upload_time = "2025-05-02T14:54:09.696Z" }, - { url = "https://files.pythonhosted.org/packages/64/6d/0783af677e601d8a42258f0fbc47663abf435f927e58a8d2928296743099/magika-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9109309328a1553886c8ff36c2ee9a5e9cfd36893ad81b65bf61a57debdd9d0e", size = 12404787, upload_time = "2025-05-02T14:54:16.963Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ad/42e39748ddc4bbe55c2dc1093ce29079c04d096ac0d844f8ae66178bc3ed/magika-0.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:57cd1d64897634d15de552bd6b3ae9c6ff6ead9c60d384dc46497c08288e4559", size = 15091089, upload_time = "2025-05-02T14:54:11.59Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1f/28e412d0ccedc068fbccdae6a6233faaa97ec3e5e2ffd242e49655b10064/magika-0.6.2-py3-none-win_amd64.whl", hash = "sha256:711f427a633e0182737dcc2074748004842f870643585813503ff2553b973b9f", size = 12385740, upload_time = "2025-05-02T14:54:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/07/4f7748f34279f2852068256992377474f9700b6fbad6735d6be58605178f/magika-0.6.2-py3-none-any.whl", hash = "sha256:5ef72fbc07723029b3684ef81454bc224ac5f60986aa0fc5a28f4456eebcb5b2", size = 2967609, upload-time = "2025-05-02T14:54:09.696Z" }, + { url = "https://files.pythonhosted.org/packages/64/6d/0783af677e601d8a42258f0fbc47663abf435f927e58a8d2928296743099/magika-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9109309328a1553886c8ff36c2ee9a5e9cfd36893ad81b65bf61a57debdd9d0e", size = 12404787, upload-time = "2025-05-02T14:54:16.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ad/42e39748ddc4bbe55c2dc1093ce29079c04d096ac0d844f8ae66178bc3ed/magika-0.6.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:57cd1d64897634d15de552bd6b3ae9c6ff6ead9c60d384dc46497c08288e4559", size = 15091089, upload-time = "2025-05-02T14:54:11.59Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1f/28e412d0ccedc068fbccdae6a6233faaa97ec3e5e2ffd242e49655b10064/magika-0.6.2-py3-none-win_amd64.whl", hash = "sha256:711f427a633e0182737dcc2074748004842f870643585813503ff2553b973b9f", size = 12385740, upload-time = "2025-05-02T14:54:14.096Z" }, ] [[package]] @@ -1308,18 +1447,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cobble" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/f8/b48bf3b9c7c47f3bc0de7630f0f180c01e92570953611089489d34542253/mammoth-1.9.1.tar.gz", hash = "sha256:7924254ab8f03efe55fadc0fd5f7828db831190eb2679d63cb4372873e71c572", size = 51056, upload_time = "2025-05-28T19:17:56.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/f8/b48bf3b9c7c47f3bc0de7630f0f180c01e92570953611089489d34542253/mammoth-1.9.1.tar.gz", hash = "sha256:7924254ab8f03efe55fadc0fd5f7828db831190eb2679d63cb4372873e71c572", size = 51056, upload-time = "2025-05-28T19:17:56.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/0c/3153f159b78e368ac473a00e955d69d976e4b69740ed07c76c9f72a161b8/mammoth-1.9.1-py2.py3-none-any.whl", hash = "sha256:f0569bd640cee6c77a07e7c75c5dc10d745dc4dc95d530cfcbb0a5d9536d636c", size = 52991, upload_time = "2025-05-28T19:17:54.62Z" }, + { url = "https://files.pythonhosted.org/packages/be/0c/3153f159b78e368ac473a00e955d69d976e4b69740ed07c76c9f72a161b8/mammoth-1.9.1-py2.py3-none-any.whl", hash = "sha256:f0569bd640cee6c77a07e7c75c5dc10d745dc4dc95d530cfcbb0a5d9536d636c", size = 52991, upload-time = "2025-05-28T19:17:54.62Z" }, ] [[package]] name = "markdown" version = "3.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload_time = "2025-06-19T17:12:44.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload_time = "2025-06-19T17:12:42.994Z" }, + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, ] [[package]] @@ -1329,9 +1468,9 @@ 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, upload_time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } 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, upload_time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] @@ -1342,9 +1481,9 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload_time = "2025-03-05T11:54:40.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload_time = "2025-03-05T11:54:39.454Z" }, + { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" }, ] [[package]] @@ -1359,9 +1498,9 @@ dependencies = [ { name = "markdownify" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/bd/b7ae7863ee556411fbb6ca19a4a7593ef2b3531d6cd10b979ba386a2dd4d/markitdown-0.1.2.tar.gz", hash = "sha256:85fe108a92bd18f317e75a36cf567a6fa812072612a898abf8c156d5d74c13c4", size = 39361, upload_time = "2025-05-28T17:06:10.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/bd/b7ae7863ee556411fbb6ca19a4a7593ef2b3531d6cd10b979ba386a2dd4d/markitdown-0.1.2.tar.gz", hash = "sha256:85fe108a92bd18f317e75a36cf567a6fa812072612a898abf8c156d5d74c13c4", size = 39361, upload-time = "2025-05-28T17:06:10.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/33/d52d06b44c28e0db5c458690a4356e6abbb866f4abc00c0cf4eebb90ca78/markitdown-0.1.2-py3-none-any.whl", hash = "sha256:4881f0768794ffccb52d09dd86498813a6896ba9639b4fc15512817f56ed9d74", size = 57751, upload_time = "2025-05-28T17:06:08.722Z" }, + { url = "https://files.pythonhosted.org/packages/ed/33/d52d06b44c28e0db5c458690a4356e6abbb866f4abc00c0cf4eebb90ca78/markitdown-0.1.2-py3-none-any.whl", hash = "sha256:4881f0768794ffccb52d09dd86498813a6896ba9639b4fc15512817f56ed9d74", size = 57751, upload-time = "2025-05-28T17:06:08.722Z" }, ] [package.optional-dependencies] @@ -1385,38 +1524,38 @@ all = [ 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, upload_time = "2024-10-18T15:21:54.129Z" } -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, upload_time = "2024-10-18T15:21:13.777Z" }, - { 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, upload_time = "2024-10-18T15:21:14.822Z" }, - { 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, upload_time = "2024-10-18T15:21:15.642Z" }, - { 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, upload_time = "2024-10-18T15:21:17.133Z" }, - { 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, upload_time = "2024-10-18T15:21:18.064Z" }, - { 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, upload_time = "2024-10-18T15:21:18.859Z" }, - { 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, upload_time = "2024-10-18T15:21:19.671Z" }, - { 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, upload_time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload_time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload_time = "2024-10-18T15:21:23.499Z" }, - { 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, upload_time = "2024-10-18T15:21:24.577Z" }, - { 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, upload_time = "2024-10-18T15:21:25.382Z" }, - { 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, upload_time = "2024-10-18T15:21:26.199Z" }, - { 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, upload_time = "2024-10-18T15:21:27.029Z" }, - { 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, upload_time = "2024-10-18T15:21:27.846Z" }, - { 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, upload_time = "2024-10-18T15:21:28.744Z" }, - { 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, upload_time = "2024-10-18T15:21:29.545Z" }, - { 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, upload_time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload_time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload_time = "2024-10-18T15:21:32.032Z" }, - { 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, upload_time = "2024-10-18T15:21:33.625Z" }, - { 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, upload_time = "2024-10-18T15:21:34.611Z" }, - { 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, upload_time = "2024-10-18T15:21:35.398Z" }, - { 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, upload_time = "2024-10-18T15:21:36.231Z" }, - { 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, upload_time = "2024-10-18T15:21:37.073Z" }, - { 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, upload_time = "2024-10-18T15:21:37.932Z" }, - { 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, upload_time = "2024-10-18T15:21:39.799Z" }, - { 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, upload_time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload_time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +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, upload-time = "2024-10-18T15:21:13.777Z" }, + { 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, upload-time = "2024-10-18T15:21:14.822Z" }, + { 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, upload-time = "2024-10-18T15:21:15.642Z" }, + { 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, upload-time = "2024-10-18T15:21:17.133Z" }, + { 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, upload-time = "2024-10-18T15:21:18.064Z" }, + { 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, upload-time = "2024-10-18T15:21:18.859Z" }, + { 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, upload-time = "2024-10-18T15:21:19.671Z" }, + { 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, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { 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, upload-time = "2024-10-18T15:21:24.577Z" }, + { 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, upload-time = "2024-10-18T15:21:25.382Z" }, + { 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, upload-time = "2024-10-18T15:21:26.199Z" }, + { 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, upload-time = "2024-10-18T15:21:27.029Z" }, + { 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, upload-time = "2024-10-18T15:21:27.846Z" }, + { 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, upload-time = "2024-10-18T15:21:28.744Z" }, + { 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, upload-time = "2024-10-18T15:21:29.545Z" }, + { 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, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { 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, upload-time = "2024-10-18T15:21:33.625Z" }, + { 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, upload-time = "2024-10-18T15:21:34.611Z" }, + { 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, upload-time = "2024-10-18T15:21:35.398Z" }, + { 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, upload-time = "2024-10-18T15:21:36.231Z" }, + { 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, upload-time = "2024-10-18T15:21:37.073Z" }, + { 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, upload-time = "2024-10-18T15:21:37.932Z" }, + { 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, upload-time = "2024-10-18T15:21:39.799Z" }, + { 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, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] @@ -1426,9 +1565,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload_time = "2025-02-03T15:32:25.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload_time = "2025-02-03T15:32:22.295Z" }, + { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, ] [[package]] @@ -1447,27 +1586,27 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969, upload_time = "2025-06-27T12:03:08.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/68/63045305f29ff680a9cd5be360c755270109e6b76f696ea6824547ddbc30/mcp-1.10.1.tar.gz", hash = "sha256:aaa0957d8307feeff180da2d9d359f2b801f35c0c67f1882136239055ef034c2", size = 392969, upload-time = "2025-06-27T12:03:08.982Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878, upload_time = "2025-06-27T12:03:07.328Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3f/435a5b3d10ae242a9d6c2b33175551173c3c61fe637dc893be05c4ed0aaf/mcp-1.10.1-py3-none-any.whl", hash = "sha256:4d08301aefe906dce0fa482289db55ce1db831e3e67212e65b5e23ad8454b3c5", size = 150878, upload-time = "2025-06-27T12:03:07.328Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload_time = "2023-03-07T16:47:11.061Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload_time = "2023-03-07T16:47:09.197Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] @@ -1479,9 +1618,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload_time = "2025-04-25T13:12:34.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload_time = "2025-04-25T13:12:33.034Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, ] [[package]] @@ -1491,81 +1630,81 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload_time = "2025-03-14T23:51:03.902Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload_time = "2025-03-14T23:51:03.016Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] [[package]] name = "multidict" version = "6.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload_time = "2025-06-30T15:53:46.929Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload_time = "2025-06-30T15:51:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload_time = "2025-06-30T15:51:49.986Z" }, - { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload_time = "2025-06-30T15:51:51.331Z" }, - { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload_time = "2025-06-30T15:51:52.584Z" }, - { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload_time = "2025-06-30T15:51:53.913Z" }, - { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload_time = "2025-06-30T15:51:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload_time = "2025-06-30T15:51:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload_time = "2025-06-30T15:51:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload_time = "2025-06-30T15:52:00.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload_time = "2025-06-30T15:52:02.43Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload_time = "2025-06-30T15:52:04.26Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload_time = "2025-06-30T15:52:06.002Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload_time = "2025-06-30T15:52:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload_time = "2025-06-30T15:52:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload_time = "2025-06-30T15:52:10.947Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload_time = "2025-06-30T15:52:12.334Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload_time = "2025-06-30T15:52:13.6Z" }, - { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload_time = "2025-06-30T15:52:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload_time = "2025-06-30T15:52:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload_time = "2025-06-30T15:52:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload_time = "2025-06-30T15:52:19.346Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload_time = "2025-06-30T15:52:20.773Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload_time = "2025-06-30T15:52:22.242Z" }, - { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload_time = "2025-06-30T15:52:23.736Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload_time = "2025-06-30T15:52:25.185Z" }, - { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload_time = "2025-06-30T15:52:26.969Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload_time = "2025-06-30T15:52:28.467Z" }, - { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload_time = "2025-06-30T15:52:29.938Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload_time = "2025-06-30T15:52:31.416Z" }, - { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload_time = "2025-06-30T15:52:32.996Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload_time = "2025-06-30T15:52:34.521Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload_time = "2025-06-30T15:52:35.999Z" }, - { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload_time = "2025-06-30T15:52:37.473Z" }, - { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload_time = "2025-06-30T15:52:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload_time = "2025-06-30T15:52:40.207Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload_time = "2025-06-30T15:52:41.575Z" }, - { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload_time = "2025-06-30T15:52:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload_time = "2025-06-30T15:52:45.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload_time = "2025-06-30T15:52:46.459Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload_time = "2025-06-30T15:52:47.88Z" }, - { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload_time = "2025-06-30T15:52:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload_time = "2025-06-30T15:52:50.903Z" }, - { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload_time = "2025-06-30T15:52:52.764Z" }, - { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload_time = "2025-06-30T15:52:54.596Z" }, - { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload_time = "2025-06-30T15:52:56.175Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload_time = "2025-06-30T15:52:57.752Z" }, - { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload_time = "2025-06-30T15:52:59.74Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload_time = "2025-06-30T15:53:01.602Z" }, - { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload_time = "2025-06-30T15:53:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload_time = "2025-06-30T15:53:05.48Z" }, - { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload_time = "2025-06-30T15:53:07.522Z" }, - { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload_time = "2025-06-30T15:53:09.263Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload_time = "2025-06-30T15:53:11.038Z" }, - { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload_time = "2025-06-30T15:53:12.421Z" }, - { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload_time = "2025-06-30T15:53:45.437Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, + { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, + { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, + { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, + { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload_time = "2025-04-22T14:54:24.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload_time = "2025-04-22T14:54:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] @@ -1578,9 +1717,9 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload_time = "2024-08-18T19:48:37.769Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload_time = "2024-08-18T19:48:21.909Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" }, ] [[package]] @@ -1591,65 +1730,65 @@ dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload_time = "2025-04-09T02:58:07.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload_time = "2025-04-09T02:57:51.857Z" }, - { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload_time = "2025-04-09T02:57:53.658Z" }, - { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload_time = "2025-04-09T02:57:55.206Z" }, - { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload_time = "2025-04-09T02:57:56.818Z" }, - { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload_time = "2025-04-09T02:57:58.45Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload_time = "2025-04-09T02:57:59.96Z" }, - { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload_time = "2025-04-09T02:58:01.435Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload_time = "2025-04-09T02:58:02.933Z" }, - { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload_time = "2025-04-09T02:58:04.538Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload_time = "2025-04-09T02:58:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" }, ] [[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, upload_time = "2025-05-17T22:38:04.611Z" } -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, upload_time = "2025-05-17T21:34:39.648Z" }, - { 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, upload_time = "2025-05-17T21:35:01.241Z" }, - { 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, upload_time = "2025-05-17T21:35:10.622Z" }, - { 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, upload_time = "2025-05-17T21:35:21.414Z" }, - { 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, upload_time = "2025-05-17T21:35:42.174Z" }, - { 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, upload_time = "2025-05-17T21:36:06.711Z" }, - { 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, upload_time = "2025-05-17T21:36:29.965Z" }, - { 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, upload_time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload_time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload_time = "2025-05-17T21:37:26.213Z" }, - { 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, upload_time = "2025-05-17T21:37:56.699Z" }, - { 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, upload_time = "2025-05-17T21:38:18.291Z" }, - { 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, upload_time = "2025-05-17T21:38:27.319Z" }, - { 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, upload_time = "2025-05-17T21:38:38.141Z" }, - { 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, upload_time = "2025-05-17T21:38:58.433Z" }, - { 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, upload_time = "2025-05-17T21:39:22.638Z" }, - { 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, upload_time = "2025-05-17T21:39:45.865Z" }, - { 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, upload_time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload_time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload_time = "2025-05-17T21:44:05.145Z" }, - { 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, upload_time = "2025-05-17T21:40:44Z" }, - { 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, upload_time = "2025-05-17T21:41:05.695Z" }, - { 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, upload_time = "2025-05-17T21:41:15.903Z" }, - { 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, upload_time = "2025-05-17T21:41:27.321Z" }, - { 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, upload_time = "2025-05-17T21:41:49.738Z" }, - { 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, upload_time = "2025-05-17T21:42:14.046Z" }, - { 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, upload_time = "2025-05-17T21:42:37.464Z" }, - { 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, upload_time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload_time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload_time = "2025-05-17T21:43:35.479Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +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, upload-time = "2025-05-17T21:34:39.648Z" }, + { 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, upload-time = "2025-05-17T21:35:01.241Z" }, + { 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, upload-time = "2025-05-17T21:35:10.622Z" }, + { 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, upload-time = "2025-05-17T21:35:21.414Z" }, + { 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, upload-time = "2025-05-17T21:35:42.174Z" }, + { 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, upload-time = "2025-05-17T21:36:06.711Z" }, + { 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, upload-time = "2025-05-17T21:36:29.965Z" }, + { 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, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { 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, upload-time = "2025-05-17T21:37:56.699Z" }, + { 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, upload-time = "2025-05-17T21:38:18.291Z" }, + { 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, upload-time = "2025-05-17T21:38:27.319Z" }, + { 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, upload-time = "2025-05-17T21:38:38.141Z" }, + { 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, upload-time = "2025-05-17T21:38:58.433Z" }, + { 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, upload-time = "2025-05-17T21:39:22.638Z" }, + { 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, upload-time = "2025-05-17T21:39:45.865Z" }, + { 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, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { 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, upload-time = "2025-05-17T21:40:44Z" }, + { 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, upload-time = "2025-05-17T21:41:05.695Z" }, + { 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, upload-time = "2025-05-17T21:41:15.903Z" }, + { 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, upload-time = "2025-05-17T21:41:27.321Z" }, + { 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, upload-time = "2025-05-17T21:41:49.738Z" }, + { 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, upload-time = "2025-05-17T21:42:14.046Z" }, + { 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, upload-time = "2025-05-17T21:42:37.464Z" }, + { 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, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, ] [[package]] name = "olefile" version = "0.47" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload_time = "2023-12-01T16:22:53.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload_time = "2023-12-01T16:22:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, ] [[package]] @@ -1665,16 +1804,16 @@ dependencies = [ { name = "sympy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046, upload_time = "2025-05-09T20:26:02.399Z" }, - { url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220, upload_time = "2025-05-09T20:25:47.078Z" }, - { url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377, upload_time = "2025-05-09T20:26:14.478Z" }, - { url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233, upload_time = "2025-05-12T21:26:16.963Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/5cb5018d5b0b7cba820d2c4a1d1b02d40df538d49138ba36a509457e4df6/onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07", size = 34298715, upload_time = "2025-05-09T20:26:05.634Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/1dfe1b368831d1256b90b95cb8d11da8ab769febd5c8833ec85ec1f79d21/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919", size = 14443266, upload_time = "2025-05-09T20:25:49.479Z" }, - { url = "https://files.pythonhosted.org/packages/1e/70/342514ade3a33ad9dd505dcee96ff1f0e7be6d0e6e9c911fe0f1505abf42/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8", size = 16406707, upload_time = "2025-05-09T20:26:17.454Z" }, - { url = "https://files.pythonhosted.org/packages/3e/89/2f64e250945fa87140fb917ba377d6d0e9122e029c8512f389a9b7f953f4/onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b", size = 12691777, upload_time = "2025-05-12T21:26:20.19Z" }, - { url = "https://files.pythonhosted.org/packages/9f/48/d61d5f1ed098161edd88c56cbac49207d7b7b149e613d2cd7e33176c63b3/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed", size = 14454003, upload_time = "2025-05-09T20:25:52.287Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/873b955beda7bada5b0d798d3a601b2ff210e44ad5169f6d405b93892103/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36", size = 16427482, upload_time = "2025-05-09T20:26:20.376Z" }, + { url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046, upload-time = "2025-05-09T20:26:02.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220, upload-time = "2025-05-09T20:25:47.078Z" }, + { url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377, upload-time = "2025-05-09T20:26:14.478Z" }, + { url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233, upload-time = "2025-05-12T21:26:16.963Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/5cb5018d5b0b7cba820d2c4a1d1b02d40df538d49138ba36a509457e4df6/onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07", size = 34298715, upload-time = "2025-05-09T20:26:05.634Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/1dfe1b368831d1256b90b95cb8d11da8ab769febd5c8833ec85ec1f79d21/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919", size = 14443266, upload-time = "2025-05-09T20:25:49.479Z" }, + { url = "https://files.pythonhosted.org/packages/1e/70/342514ade3a33ad9dd505dcee96ff1f0e7be6d0e6e9c911fe0f1505abf42/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8", size = 16406707, upload-time = "2025-05-09T20:26:17.454Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/2f64e250945fa87140fb917ba377d6d0e9122e029c8512f389a9b7f953f4/onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b", size = 12691777, upload-time = "2025-05-12T21:26:20.19Z" }, + { url = "https://files.pythonhosted.org/packages/9f/48/d61d5f1ed098161edd88c56cbac49207d7b7b149e613d2cd7e33176c63b3/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed", size = 14454003, upload-time = "2025-05-09T20:25:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/873b955beda7bada5b0d798d3a601b2ff210e44ad5169f6d405b93892103/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36", size = 16427482, upload-time = "2025-05-09T20:26:20.376Z" }, ] [[package]] @@ -1691,9 +1830,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/d7/e91c6a9cf71726420cddf539852ee4c29176ebb716a702d9118d0409fd8e/openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337", size = 486573, upload_time = "2025-06-27T21:21:39.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/d7/e91c6a9cf71726420cddf539852ee4c29176ebb716a702d9118d0409fd8e/openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337", size = 486573, upload-time = "2025-06-27T21:21:39.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/46/a10d9df4673df56f71201d129ba1cb19eaff3366d08c8664d61a7df52e65/openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090", size = 755038, upload_time = "2025-06-27T21:21:37.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/46/a10d9df4673df56f71201d129ba1cb19eaff3366d08c8664d61a7df52e65/openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090", size = 755038, upload-time = "2025-06-27T21:21:37.532Z" }, ] [[package]] @@ -1703,9 +1842,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload_time = "2025-01-08T19:29:27.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload_time = "2025-01-08T19:29:25.275Z" }, + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] [[package]] @@ -1715,56 +1854,56 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "et-xmlfile" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload_time = "2024-06-28T14:03:44.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload_time = "2024-06-28T14:03:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] [[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, upload_time = "2025-04-29T23:30:08.423Z" } -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, upload_time = "2025-04-29T23:28:53.612Z" }, - { 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, upload_time = "2025-04-29T23:28:55.055Z" }, - { 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, upload_time = "2025-04-29T23:28:56.828Z" }, - { 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, upload_time = "2025-04-29T23:28:58.751Z" }, - { 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, upload_time = "2025-04-29T23:29:00.129Z" }, - { 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, upload_time = "2025-04-29T23:29:01.704Z" }, - { 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, upload_time = "2025-04-29T23:29:03.576Z" }, - { 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, upload_time = "2025-04-29T23:29:05.753Z" }, - { 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, upload_time = "2025-04-29T23:29:07.35Z" }, - { 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, upload_time = "2025-04-29T23:29:09.301Z" }, - { 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, upload_time = "2025-04-29T23:29:10.813Z" }, - { 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, upload_time = "2025-04-29T23:29:12.26Z" }, - { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload_time = "2025-04-29T23:29:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload_time = "2025-04-29T23:29:15.338Z" }, - { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload_time = "2025-04-29T23:29:17.324Z" }, - { 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, upload_time = "2025-04-29T23:29:19.083Z" }, - { 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, upload_time = "2025-04-29T23:29:20.602Z" }, - { 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, upload_time = "2025-04-29T23:29:22.062Z" }, - { 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, upload_time = "2025-04-29T23:29:23.602Z" }, - { 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, upload_time = "2025-04-29T23:29:25.094Z" }, - { 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, upload_time = "2025-04-29T23:29:26.609Z" }, - { 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, upload_time = "2025-04-29T23:29:28.153Z" }, - { 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, upload_time = "2025-04-29T23:29:29.726Z" }, - { 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, upload_time = "2025-04-29T23:29:31.269Z" }, - { 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, upload_time = "2025-04-29T23:29:33.315Z" }, - { 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, upload_time = "2025-04-29T23:29:34.946Z" }, - { 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, upload_time = "2025-04-29T23:29:36.52Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload_time = "2025-04-29T23:29:38.292Z" }, - { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload_time = "2025-04-29T23:29:40.349Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload_time = "2025-04-29T23:29:41.922Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" } +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, upload-time = "2025-04-29T23:28:53.612Z" }, + { 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, upload-time = "2025-04-29T23:28:55.055Z" }, + { 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, upload-time = "2025-04-29T23:28:56.828Z" }, + { 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, upload-time = "2025-04-29T23:28:58.751Z" }, + { 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, upload-time = "2025-04-29T23:29:00.129Z" }, + { 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, upload-time = "2025-04-29T23:29:01.704Z" }, + { 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, upload-time = "2025-04-29T23:29:03.576Z" }, + { 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, upload-time = "2025-04-29T23:29:05.753Z" }, + { 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, upload-time = "2025-04-29T23:29:07.35Z" }, + { 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, upload-time = "2025-04-29T23:29:09.301Z" }, + { 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, upload-time = "2025-04-29T23:29:10.813Z" }, + { 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, upload-time = "2025-04-29T23:29:12.26Z" }, + { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload-time = "2025-04-29T23:29:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload-time = "2025-04-29T23:29:15.338Z" }, + { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload-time = "2025-04-29T23:29:17.324Z" }, + { 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, upload-time = "2025-04-29T23:29:19.083Z" }, + { 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, upload-time = "2025-04-29T23:29:20.602Z" }, + { 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, upload-time = "2025-04-29T23:29:22.062Z" }, + { 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, upload-time = "2025-04-29T23:29:23.602Z" }, + { 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, upload-time = "2025-04-29T23:29:25.094Z" }, + { 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, upload-time = "2025-04-29T23:29:26.609Z" }, + { 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, upload-time = "2025-04-29T23:29:28.153Z" }, + { 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, upload-time = "2025-04-29T23:29:29.726Z" }, + { 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, upload-time = "2025-04-29T23:29:31.269Z" }, + { 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, upload-time = "2025-04-29T23:29:33.315Z" }, + { 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, upload-time = "2025-04-29T23:29:34.946Z" }, + { 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, upload-time = "2025-04-29T23:29:36.52Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" }, ] [[package]] name = "packaging" version = "24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload_time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload_time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] [[package]] @@ -1777,28 +1916,28 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload_time = "2025-06-05T03:27:54.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload_time = "2025-06-05T03:26:46.774Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload_time = "2025-06-05T16:50:14.439Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload_time = "2025-06-05T16:50:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload_time = "2025-06-05T03:26:51.813Z" }, - { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload_time = "2025-06-06T00:00:18.651Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload_time = "2025-06-05T03:26:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload_time = "2025-06-05T03:26:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload_time = "2025-06-05T03:27:02.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload_time = "2025-06-05T16:50:20.17Z" }, - { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload_time = "2025-06-05T03:27:06.431Z" }, - { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload_time = "2025-06-05T03:27:09.875Z" }, - { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload_time = "2025-06-06T00:00:22.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload_time = "2025-06-05T03:27:15.641Z" }, - { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload_time = "2025-06-05T03:27:24.131Z" }, - { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload_time = "2025-06-05T03:27:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload_time = "2025-06-05T03:27:39.448Z" }, - { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload_time = "2025-06-05T03:27:43.652Z" }, - { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload_time = "2025-06-05T03:27:47.652Z" }, - { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload_time = "2025-06-06T00:00:26.142Z" }, - { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload_time = "2025-06-05T03:27:51.465Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, + { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, + { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, ] [[package]] @@ -1809,164 +1948,199 @@ dependencies = [ { name = "charset-normalizer" }, { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/46/5223d613ac4963e1f7c07b2660fe0e9e770102ec6bda8c038400113fb215/pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7", size = 7387678, upload_time = "2025-05-06T16:17:00.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/46/5223d613ac4963e1f7c07b2660fe0e9e770102ec6bda8c038400113fb215/pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7", size = 7387678, upload-time = "2025-05-06T16:17:00.787Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/16/7a432c0101fa87457e75cb12c879e1749c5870a786525e2e0f42871d6462/pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3", size = 5620187, upload_time = "2025-05-06T16:16:58.669Z" }, + { url = "https://files.pythonhosted.org/packages/73/16/7a432c0101fa87457e75cb12c879e1749c5870a786525e2e0f42871d6462/pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3", size = 5620187, upload-time = "2025-05-06T16:16:58.669Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload_time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload_time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload_time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload_time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload_time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload_time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload_time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload_time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload_time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload_time = "2025-07-01T09:14:30.104Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload_time = "2025-07-01T09:14:31.899Z" }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload_time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload_time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload_time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload_time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload_time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload_time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload_time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload_time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload_time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload_time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload_time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload_time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload_time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload_time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload_time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload_time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload_time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload_time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload_time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload_time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload_time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload_time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload_time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload_time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload_time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload_time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload_time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload_time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload_time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload_time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload_time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload_time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload_time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload_time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload_time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload_time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload_time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload_time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload_time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload_time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload_time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload_time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload_time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload_time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload_time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload_time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload_time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload_time = "2025-07-01T09:15:50.399Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, +] + +[[package]] +name = "pip" +version = "25.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/6e/74a3f0179a4a73a53d66ce57fdb4de0080a8baa1de0063de206d6167acc2/pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343", size = 1803014, upload-time = "2025-10-25T00:55:41.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/3c/d717024885424591d5376220b5e836c2d5293ce2011523c9de23ff7bf068/pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd", size = 1778622, upload-time = "2025-10-25T00:55:39.247Z" }, +] + +[[package]] +name = "pip-tools" +version = "7.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "build" }, + { name = "click" }, + { name = "pip" }, + { name = "pyproject-hooks" }, + { name = "setuptools" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/d149fb40bc425ad9defcb8ff73c65088bbc36a84b1825e035397d1c40624/pip_tools-7.5.2.tar.gz", hash = "sha256:2d64d72da6a044da1110257d333960563d7a4743637e8617dd2610ae7b82d60f", size = 164815, upload-time = "2025-11-12T22:46:12.627Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/c1/61aef9517201b43cc20f4a5c9339a072644cbaf0e9ce4e4970c2a105f766/pip_tools-7.5.2-py3-none-any.whl", hash = "sha256:2fe16db727bbe5bf28765aeb581e792e61be51fc275545ef6725374ad720a1ce", size = 66905, upload-time = "2025-11-12T22:46:11.374Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pocketflow" version = "0.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/7f/61e567a2dc1d6a8e9999e6b14d593247bc902dc8b0797e0c5ee9b531f50c/pocketflow-0.0.2.tar.gz", hash = "sha256:9249f05223f1b325a99fa4445d5ab69c89afa1e0e4acecdc1e6109936529e1b7", size = 17897, upload_time = "2025-04-11T19:25:29.718Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/7f/61e567a2dc1d6a8e9999e6b14d593247bc902dc8b0797e0c5ee9b531f50c/pocketflow-0.0.2.tar.gz", hash = "sha256:9249f05223f1b325a99fa4445d5ab69c89afa1e0e4acecdc1e6109936529e1b7", size = 17897, upload-time = "2025-04-11T19:25:29.718Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/a0/3898b9821e571d59aa248c841ed6b5a0197ea813fc0c09623c780d93eee6/pocketflow-0.0.2-py3-none-any.whl", hash = "sha256:16f9cc5cfd68b6e552c736e5b965209b1f464233c62061a2dc3080cb48a22e74", size = 3340, upload_time = "2025-04-11T19:25:28.312Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/3898b9821e571d59aa248c841ed6b5a0197ea813fc0c09623c780d93eee6/pocketflow-0.0.2-py3-none-any.whl", hash = "sha256:16f9cc5cfd68b6e552c736e5b965209b1f464233c62061a2dc3080cb48a22e74", size = 3340, upload-time = "2025-04-11T19:25:28.312Z" }, ] [[package]] name = "propcache" version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload_time = "2025-06-09T22:56:06.081Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload_time = "2025-06-09T22:54:30.551Z" }, - { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload_time = "2025-06-09T22:54:32.296Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload_time = "2025-06-09T22:54:33.929Z" }, - { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload_time = "2025-06-09T22:54:35.186Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload_time = "2025-06-09T22:54:36.708Z" }, - { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload_time = "2025-06-09T22:54:38.062Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload_time = "2025-06-09T22:54:39.634Z" }, - { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload_time = "2025-06-09T22:54:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload_time = "2025-06-09T22:54:43.038Z" }, - { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload_time = "2025-06-09T22:54:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload_time = "2025-06-09T22:54:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload_time = "2025-06-09T22:54:47.63Z" }, - { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload_time = "2025-06-09T22:54:48.982Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload_time = "2025-06-09T22:54:50.424Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload_time = "2025-06-09T22:54:52.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload_time = "2025-06-09T22:54:53.234Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload_time = "2025-06-09T22:54:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload_time = "2025-06-09T22:54:55.642Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload_time = "2025-06-09T22:54:57.246Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload_time = "2025-06-09T22:54:58.975Z" }, - { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload_time = "2025-06-09T22:55:00.471Z" }, - { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload_time = "2025-06-09T22:55:01.834Z" }, - { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload_time = "2025-06-09T22:55:03.199Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload_time = "2025-06-09T22:55:04.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload_time = "2025-06-09T22:55:05.942Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload_time = "2025-06-09T22:55:07.792Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload_time = "2025-06-09T22:55:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload_time = "2025-06-09T22:55:10.62Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload_time = "2025-06-09T22:55:12.029Z" }, - { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload_time = "2025-06-09T22:55:13.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload_time = "2025-06-09T22:55:15.284Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload_time = "2025-06-09T22:55:16.445Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload_time = "2025-06-09T22:55:17.598Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload_time = "2025-06-09T22:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload_time = "2025-06-09T22:55:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload_time = "2025-06-09T22:55:21.5Z" }, - { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload_time = "2025-06-09T22:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload_time = "2025-06-09T22:55:24.651Z" }, - { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload_time = "2025-06-09T22:55:26.049Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload_time = "2025-06-09T22:55:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload_time = "2025-06-09T22:55:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload_time = "2025-06-09T22:55:30.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload_time = "2025-06-09T22:55:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload_time = "2025-06-09T22:55:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload_time = "2025-06-09T22:55:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload_time = "2025-06-09T22:55:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload_time = "2025-06-09T22:55:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload_time = "2025-06-09T22:55:39.687Z" }, - { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload_time = "2025-06-09T22:56:04.484Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[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, upload_time = "2025-05-28T19:25:54.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload_time = "2025-05-28T19:25:41.198Z" }, - { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload_time = "2025-05-28T19:25:44.275Z" }, - { 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, upload_time = "2025-05-28T19:25:45.702Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload_time = "2025-05-28T19:25:47.128Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload_time = "2025-05-28T19:25:50.036Z" }, - { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload_time = "2025-05-28T19:25:53.926Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { 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, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload_time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload_time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] @@ -1976,18 +2150,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload_time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload_time = "2025-03-28T02:41:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload_time = "2024-03-30T13:22:22.564Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload_time = "2024-03-30T13:22:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] @@ -2000,9 +2174,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload_time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload_time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] [package.optional-dependencies] @@ -2017,39 +2191,39 @@ 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, upload_time = "2025-04-23T18:33:52.104Z" } -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, upload_time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload_time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload_time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload_time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload_time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload_time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload_time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload_time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload_time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload_time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload_time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload_time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload_time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload_time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload_time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload_time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload_time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload_time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload_time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload_time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload_time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload_time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload_time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload_time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload_time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload_time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload_time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload_time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload_time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload_time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload_time = "2025-04-23T18:32:25.088Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, ] [[package]] @@ -2061,18 +2235,18 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload_time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload_time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, ] [[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, upload_time = "2021-03-10T02:09:54.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload_time = "2021-03-10T02:09:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, ] [[package]] @@ -2083,27 +2257,27 @@ dependencies = [ { name = "cattrs" }, { name = "lsprotocol" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527, upload_time = "2024-03-26T18:44:25.679Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/b9/41d173dad9eaa9db9c785a85671fc3d68961f08d67706dc2e79011e10b5c/pygls-1.3.1.tar.gz", hash = "sha256:140edceefa0da0e9b3c533547c892a42a7d2fd9217ae848c330c53d266a55018", size = 45527, upload-time = "2024-03-26T18:44:25.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031, upload_time = "2024-03-26T18:44:24.249Z" }, + { url = "https://files.pythonhosted.org/packages/11/19/b74a10dd24548e96e8c80226cbacb28b021bc3a168a7d2709fb0d0185348/pygls-1.3.1-py3-none-any.whl", hash = "sha256:6e00f11efc56321bdeb6eac04f6d86131f654c7d49124344a9ebb968da3dd91e", size = 56031, upload-time = "2024-03-26T18:44:24.249Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload_time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload_time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload_time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload_time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] [package.optional-dependencies] @@ -2115,24 +2289,76 @@ crypto = [ name = "pymupdf" version = "1.26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/d4/70a265e4bcd43e97480ae62da69396ef4507c8f9cfd179005ee731c92a04/pymupdf-1.26.3.tar.gz", hash = "sha256:b7d2c3ffa9870e1e4416d18862f5ccd356af5fe337b4511093bbbce2ca73b7e5", size = 75990308, upload_time = "2025-07-02T21:34:22.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/d4/70a265e4bcd43e97480ae62da69396ef4507c8f9cfd179005ee731c92a04/pymupdf-1.26.3.tar.gz", hash = "sha256:b7d2c3ffa9870e1e4416d18862f5ccd356af5fe337b4511093bbbce2ca73b7e5", size = 75990308, upload-time = "2025-07-02T21:34:22.243Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/d3/c7af70545cd3097a869fd635bb6222108d3a0fb28c0b8254754a126c4cbb/pymupdf-1.26.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ded891963944e5f13b03b88f6d9e982e816a4ec8689fe360876eef000c161f2b", size = 23057205, upload_time = "2025-07-02T21:26:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/04/3d/ec5b69bfeaa5deefa7141fc0b20d77bb20404507cf17196b4eb59f1f2977/pymupdf-1.26.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:436a33c738bb10eadf00395d18a6992b801ffb26521ee1f361ae786dd283327a", size = 22406630, upload_time = "2025-07-02T21:27:10.112Z" }, - { url = "https://files.pythonhosted.org/packages/fc/20/661d3894bb05ad75ed6ca103ee2c3fa44d88a458b5c8d4a946b9c0f2569b/pymupdf-1.26.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a2d7a3cd442f12f05103cb3bb1415111517f0a97162547a3720f3bbbc5e0b51c", size = 23450287, upload_time = "2025-07-03T07:22:19.317Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7f/21828f018e65b16a033731d21f7b46d93fa81c6e8257f769ca4a1c2a1cb0/pymupdf-1.26.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:454f38c8cf07eb333eb4646dca10517b6e90f57ce2daa2265a78064109d85555", size = 24057319, upload_time = "2025-07-02T21:28:26.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/e8f88cd5a45b8f5fa6590ce8cef3ce0fad30eac6aac8aea12406f95bee7d/pymupdf-1.26.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:759b75d2f710ff4edf8d097d2e98f60e9ecef47632cead6f949b3412facdb9f0", size = 24261350, upload_time = "2025-07-02T21:29:21.733Z" }, - { url = "https://files.pythonhosted.org/packages/82/22/ecc560e4f281b5dffafbf3a81f023d268b1746d028044f495115b74a2e70/pymupdf-1.26.3-cp39-abi3-win32.whl", hash = "sha256:a839ed44742faa1cd4956bb18068fe5aae435d67ce915e901318646c4e7bbea6", size = 17116371, upload_time = "2025-07-02T21:30:23.253Z" }, - { url = "https://files.pythonhosted.org/packages/4a/26/8c72973b8833a72785cedc3981eb59b8ac7075942718bbb7b69b352cdde4/pymupdf-1.26.3-cp39-abi3-win_amd64.whl", hash = "sha256:b4cd5124d05737944636cf45fc37ce5824f10e707b0342efe109c7b6bd37a9cc", size = 18735124, upload_time = "2025-07-02T21:31:10.992Z" }, + { url = "https://files.pythonhosted.org/packages/70/d3/c7af70545cd3097a869fd635bb6222108d3a0fb28c0b8254754a126c4cbb/pymupdf-1.26.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ded891963944e5f13b03b88f6d9e982e816a4ec8689fe360876eef000c161f2b", size = 23057205, upload-time = "2025-07-02T21:26:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/04/3d/ec5b69bfeaa5deefa7141fc0b20d77bb20404507cf17196b4eb59f1f2977/pymupdf-1.26.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:436a33c738bb10eadf00395d18a6992b801ffb26521ee1f361ae786dd283327a", size = 22406630, upload-time = "2025-07-02T21:27:10.112Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/661d3894bb05ad75ed6ca103ee2c3fa44d88a458b5c8d4a946b9c0f2569b/pymupdf-1.26.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a2d7a3cd442f12f05103cb3bb1415111517f0a97162547a3720f3bbbc5e0b51c", size = 23450287, upload-time = "2025-07-03T07:22:19.317Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7f/21828f018e65b16a033731d21f7b46d93fa81c6e8257f769ca4a1c2a1cb0/pymupdf-1.26.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:454f38c8cf07eb333eb4646dca10517b6e90f57ce2daa2265a78064109d85555", size = 24057319, upload-time = "2025-07-02T21:28:26.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/e8f88cd5a45b8f5fa6590ce8cef3ce0fad30eac6aac8aea12406f95bee7d/pymupdf-1.26.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:759b75d2f710ff4edf8d097d2e98f60e9ecef47632cead6f949b3412facdb9f0", size = 24261350, upload-time = "2025-07-02T21:29:21.733Z" }, + { url = "https://files.pythonhosted.org/packages/82/22/ecc560e4f281b5dffafbf3a81f023d268b1746d028044f495115b74a2e70/pymupdf-1.26.3-cp39-abi3-win32.whl", hash = "sha256:a839ed44742faa1cd4956bb18068fe5aae435d67ce915e901318646c4e7bbea6", size = 17116371, upload-time = "2025-07-02T21:30:23.253Z" }, + { url = "https://files.pythonhosted.org/packages/4a/26/8c72973b8833a72785cedc3981eb59b8ac7075942718bbb7b69b352cdde4/pymupdf-1.26.3-cp39-abi3-win_amd64.whl", hash = "sha256:b4cd5124d05737944636cf45fc37ce5824f10e707b0342efe109c7b6bd37a9cc", size = 18735124, upload-time = "2025-07-02T21:31:10.992Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, ] [[package]] name = "pyreadline3" version = "3.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload_time = "2024-09-19T02:40:10.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload_time = "2024-09-19T02:40:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -2142,36 +2368,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload_time = "2024-03-01T18:36:20.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload_time = "2024-03-01T18:36:18.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload_time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload_time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] name = "python-json-logger" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload_time = "2025-03-07T07:08:27.301Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload_time = "2025-03-07T07:08:25.627Z" }, + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, ] [[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, upload_time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] [[package]] @@ -2184,44 +2410,44 @@ dependencies = [ { name = "typing-extensions" }, { name = "xlsxwriter" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload_time = "2024-08-07T17:33:37.772Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/a9/0c0db8d37b2b8a645666f7fd8accea4c6224e013c42b1d5c17c93590cd06/python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095", size = 10109297, upload-time = "2024-08-07T17:33:37.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload_time = "2024-08-07T17:33:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, ] [[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, upload_time = "2025-03-25T02:25:00.538Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload_time = "2025-03-25T02:24:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[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, upload_time = "2024-08-06T20:33:50.674Z" } -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, upload_time = "2024-08-06T20:32:25.131Z" }, - { 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, upload_time = "2024-08-06T20:32:26.511Z" }, - { 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, upload_time = "2024-08-06T20:32:28.363Z" }, - { 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, upload_time = "2024-08-06T20:32:30.058Z" }, - { 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, upload_time = "2024-08-06T20:32:31.881Z" }, - { 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, upload_time = "2024-08-06T20:32:37.083Z" }, - { 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, upload_time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload_time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload_time = "2024-08-06T20:32:41.93Z" }, - { 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, upload_time = "2024-08-06T20:32:43.4Z" }, - { 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, upload_time = "2024-08-06T20:32:44.801Z" }, - { 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, upload_time = "2024-08-06T20:32:46.432Z" }, - { 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, upload_time = "2024-08-06T20:32:51.188Z" }, - { 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, upload_time = "2024-08-06T20:32:53.019Z" }, - { 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, upload_time = "2024-08-06T20:32:54.708Z" }, - { 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, upload_time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +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, upload-time = "2024-08-06T20:32:25.131Z" }, + { 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, upload-time = "2024-08-06T20:32:26.511Z" }, + { 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, upload-time = "2024-08-06T20:32:28.363Z" }, + { 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, upload-time = "2024-08-06T20:32:30.058Z" }, + { 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, upload-time = "2024-08-06T20:32:31.881Z" }, + { 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, upload-time = "2024-08-06T20:32:37.083Z" }, + { 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, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { 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, upload-time = "2024-08-06T20:32:43.4Z" }, + { 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, upload-time = "2024-08-06T20:32:44.801Z" }, + { 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, upload-time = "2024-08-06T20:32:46.432Z" }, + { 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, upload-time = "2024-08-06T20:32:51.188Z" }, + { 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, upload-time = "2024-08-06T20:32:53.019Z" }, + { 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, upload-time = "2024-08-06T20:32:54.708Z" }, + { 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, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] @@ -2231,9 +2457,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4a/fa/f54f5662e0eababf0c49e92fd94bf178888562c0e7b677c8941bbbcd1bd6/querystring_parser-1.2.4.tar.gz", hash = "sha256:644fce1cffe0530453b43a83a38094dbe422ccba8c9b2f2a1c00280e14ca8a62", size = 5454, upload_time = "2019-07-22T17:58:29.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/fa/f54f5662e0eababf0c49e92fd94bf178888562c0e7b677c8941bbbcd1bd6/querystring_parser-1.2.4.tar.gz", hash = "sha256:644fce1cffe0530453b43a83a38094dbe422ccba8c9b2f2a1c00280e14ca8a62", size = 5454, upload-time = "2019-07-22T17:58:29.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/6b/572b2590fd55114118bf08bde63c0a421dcc82d593700f3e2ad89908a8a9/querystring_parser-1.2.4-py2.py3-none-any.whl", hash = "sha256:d2fa90765eaf0de96c8b087872991a10238e89ba015ae59fedfed6bd61c242a0", size = 7908, upload_time = "2020-10-21T22:33:33.17Z" }, + { url = "https://files.pythonhosted.org/packages/88/6b/572b2590fd55114118bf08bde63c0a421dcc82d593700f3e2ad89908a8a9/querystring_parser-1.2.4-py2.py3-none-any.whl", hash = "sha256:d2fa90765eaf0de96c8b087872991a10238e89ba015ae59fedfed6bd61c242a0", size = 7908, upload-time = "2020-10-21T22:33:33.17Z" }, ] [[package]] @@ -2245,47 +2471,47 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload_time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload_time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] 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, upload_time = "2024-11-06T20:12:31.635Z" } -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, upload_time = "2024-11-06T20:10:07.07Z" }, - { 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, upload_time = "2024-11-06T20:10:09.117Z" }, - { 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, upload_time = "2024-11-06T20:10:11.155Z" }, - { 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, upload_time = "2024-11-06T20:10:13.24Z" }, - { 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, upload_time = "2024-11-06T20:10:15.37Z" }, - { 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, upload_time = "2024-11-06T20:10:19.027Z" }, - { 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, upload_time = "2024-11-06T20:10:21.85Z" }, - { 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, upload_time = "2024-11-06T20:10:24.329Z" }, - { 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, upload_time = "2024-11-06T20:10:28.067Z" }, - { 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, upload_time = "2024-11-06T20:10:31.612Z" }, - { 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, upload_time = "2024-11-06T20:10:34.054Z" }, - { 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, upload_time = "2024-11-06T20:10:36.142Z" }, - { 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, upload_time = "2024-11-06T20:10:38.394Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload_time = "2024-11-06T20:10:40.367Z" }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload_time = "2024-11-06T20:10:43.467Z" }, - { 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, upload_time = "2024-11-06T20:10:45.19Z" }, - { 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, upload_time = "2024-11-06T20:10:47.177Z" }, - { 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, upload_time = "2024-11-06T20:10:49.312Z" }, - { 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, upload_time = "2024-11-06T20:10:51.102Z" }, - { 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, upload_time = "2024-11-06T20:10:52.926Z" }, - { 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, upload_time = "2024-11-06T20:10:54.828Z" }, - { 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, upload_time = "2024-11-06T20:10:56.634Z" }, - { 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, upload_time = "2024-11-06T20:10:59.369Z" }, - { 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, upload_time = "2024-11-06T20:11:02.042Z" }, - { 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, upload_time = "2024-11-06T20:11:03.933Z" }, - { 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, upload_time = "2024-11-06T20:11:06.497Z" }, - { 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, upload_time = "2024-11-06T20:11:09.06Z" }, - { 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, upload_time = "2024-11-06T20:11:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload_time = "2024-11-06T20:11:13.161Z" }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload_time = "2024-11-06T20:11:15Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } +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, upload-time = "2024-11-06T20:10:07.07Z" }, + { 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, upload-time = "2024-11-06T20:10:09.117Z" }, + { 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, upload-time = "2024-11-06T20:10:11.155Z" }, + { 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, upload-time = "2024-11-06T20:10:13.24Z" }, + { 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, upload-time = "2024-11-06T20:10:15.37Z" }, + { 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, upload-time = "2024-11-06T20:10:19.027Z" }, + { 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, upload-time = "2024-11-06T20:10:21.85Z" }, + { 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, upload-time = "2024-11-06T20:10:24.329Z" }, + { 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, upload-time = "2024-11-06T20:10:28.067Z" }, + { 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, upload-time = "2024-11-06T20:10:31.612Z" }, + { 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, upload-time = "2024-11-06T20:10:34.054Z" }, + { 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, upload-time = "2024-11-06T20:10:36.142Z" }, + { 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, upload-time = "2024-11-06T20:10:38.394Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" }, + { 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, upload-time = "2024-11-06T20:10:45.19Z" }, + { 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, upload-time = "2024-11-06T20:10:47.177Z" }, + { 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, upload-time = "2024-11-06T20:10:49.312Z" }, + { 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, upload-time = "2024-11-06T20:10:51.102Z" }, + { 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, upload-time = "2024-11-06T20:10:52.926Z" }, + { 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, upload-time = "2024-11-06T20:10:54.828Z" }, + { 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, upload-time = "2024-11-06T20:10:56.634Z" }, + { 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, upload-time = "2024-11-06T20:10:59.369Z" }, + { 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, upload-time = "2024-11-06T20:11:02.042Z" }, + { 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, upload-time = "2024-11-06T20:11:03.933Z" }, + { 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, upload-time = "2024-11-06T20:11:06.497Z" }, + { 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, upload-time = "2024-11-06T20:11:09.06Z" }, + { 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, upload-time = "2024-11-06T20:11:11.256Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" }, ] [[package]] @@ -2298,9 +2524,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload_time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload_time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] @@ -2310,9 +2536,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload_time = "2023-05-01T04:11:33.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload_time = "2023-05-01T04:11:28.427Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] @@ -2323,85 +2549,85 @@ 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, upload_time = "2025-03-30T14:15:14.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload_time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] name = "rpds-py" version = "0.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload_time = "2025-07-01T15:57:13.958Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload_time = "2025-07-01T15:54:15.734Z" }, - { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload_time = "2025-07-01T15:54:16.922Z" }, - { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload_time = "2025-07-01T15:54:18.101Z" }, - { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload_time = "2025-07-01T15:54:19.295Z" }, - { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload_time = "2025-07-01T15:54:20.858Z" }, - { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload_time = "2025-07-01T15:54:22.508Z" }, - { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload_time = "2025-07-01T15:54:23.987Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload_time = "2025-07-01T15:54:25.073Z" }, - { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload_time = "2025-07-01T15:54:26.225Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload_time = "2025-07-01T15:54:27.424Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload_time = "2025-07-01T15:54:29.93Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload_time = "2025-07-01T15:54:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload_time = "2025-07-01T15:54:32.195Z" }, - { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload_time = "2025-07-01T15:54:33.271Z" }, - { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload_time = "2025-07-01T15:54:34.755Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload_time = "2025-07-01T15:54:36.292Z" }, - { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload_time = "2025-07-01T15:54:37.469Z" }, - { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload_time = "2025-07-01T15:54:38.954Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload_time = "2025-07-01T15:54:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload_time = "2025-07-01T15:54:43.025Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload_time = "2025-07-01T15:54:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload_time = "2025-07-01T15:54:46.043Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload_time = "2025-07-01T15:54:47.64Z" }, - { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload_time = "2025-07-01T15:54:48.9Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload_time = "2025-07-01T15:54:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload_time = "2025-07-01T15:54:52.023Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload_time = "2025-07-01T15:54:53.692Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload_time = "2025-07-01T15:54:54.822Z" }, - { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload_time = "2025-07-01T15:54:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload_time = "2025-07-01T15:54:58.561Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload_time = "2025-07-01T15:54:59.751Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload_time = "2025-07-01T15:55:00.898Z" }, - { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload_time = "2025-07-01T15:55:02.201Z" }, - { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload_time = "2025-07-01T15:55:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload_time = "2025-07-01T15:55:05.398Z" }, - { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload_time = "2025-07-01T15:55:08.316Z" }, - { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload_time = "2025-07-01T15:55:09.52Z" }, - { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload_time = "2025-07-01T15:55:11.216Z" }, - { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload_time = "2025-07-01T15:55:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload_time = "2025-07-01T15:55:14.486Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload_time = "2025-07-01T15:55:15.745Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload_time = "2025-07-01T15:55:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload_time = "2025-07-01T15:55:18.922Z" }, - { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload_time = "2025-07-01T15:55:20.399Z" }, - { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload_time = "2025-07-01T15:55:21.729Z" }, - { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload_time = "2025-07-01T15:55:22.918Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload_time = "2025-07-01T15:55:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload_time = "2025-07-01T15:55:25.554Z" }, - { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload_time = "2025-07-01T15:55:27.798Z" }, - { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload_time = "2025-07-01T15:55:29.057Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload_time = "2025-07-01T15:55:30.719Z" }, - { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload_time = "2025-07-01T15:55:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload_time = "2025-07-01T15:55:33.312Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload_time = "2025-07-01T15:55:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload_time = "2025-07-01T15:55:36.202Z" }, - { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload_time = "2025-07-01T15:55:37.483Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload_time = "2025-07-01T15:55:38.828Z" }, - { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload_time = "2025-07-01T15:55:40.175Z" }, - { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload_time = "2025-07-01T15:55:42.015Z" }, - { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload_time = "2025-07-01T15:55:43.603Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload_time = "2025-07-01T15:55:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload_time = "2025-07-01T15:55:47.098Z" }, - { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload_time = "2025-07-01T15:55:48.412Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload_time = "2025-07-01T15:55:49.816Z" }, - { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload_time = "2025-07-01T15:55:51.192Z" }, - { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload_time = "2025-07-01T15:55:52.541Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload_time = "2025-07-01T15:55:53.874Z" }, - { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload_time = "2025-07-01T15:55:55.167Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, + { url = "https://files.pythonhosted.org/packages/55/07/029b7c45db910c74e182de626dfdae0ad489a949d84a468465cd0ca36355/rpds_py-0.26.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:da619979df60a940cd434084355c514c25cf8eb4cf9a508510682f6c851a4f7a", size = 364292, upload-time = "2025-07-01T15:55:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/13/d1/9b3d3f986216b4d1f584878dca15ce4797aaf5d372d738974ba737bf68d6/rpds_py-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea89a2458a1a75f87caabefe789c87539ea4e43b40f18cff526052e35bbb4fdf", size = 350334, upload-time = "2025-07-01T15:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/18/98/16d5e7bc9ec715fa9668731d0cf97f6b032724e61696e2db3d47aeb89214/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feac1045b3327a45944e7dcbeb57530339f6b17baff154df51ef8b0da34c8c12", size = 384875, upload-time = "2025-07-01T15:55:20.399Z" }, + { url = "https://files.pythonhosted.org/packages/f9/13/aa5e2b1ec5ab0e86a5c464d53514c0467bec6ba2507027d35fc81818358e/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b818a592bd69bfe437ee8368603d4a2d928c34cffcdf77c2e761a759ffd17d20", size = 399993, upload-time = "2025-07-01T15:55:21.729Z" }, + { url = "https://files.pythonhosted.org/packages/17/03/8021810b0e97923abdbab6474c8b77c69bcb4b2c58330777df9ff69dc559/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a8b0dd8648709b62d9372fc00a57466f5fdeefed666afe3fea5a6c9539a0331", size = 516683, upload-time = "2025-07-01T15:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b1/da8e61c87c2f3d836954239fdbbfb477bb7b54d74974d8f6fcb34342d166/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d3498ad0df07d81112aa6ec6c95a7e7b1ae00929fb73e7ebee0f3faaeabad2f", size = 408825, upload-time = "2025-07-01T15:55:24.207Z" }, + { url = "https://files.pythonhosted.org/packages/38/bc/1fc173edaaa0e52c94b02a655db20697cb5fa954ad5a8e15a2c784c5cbdd/rpds_py-0.26.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24a4146ccb15be237fdef10f331c568e1b0e505f8c8c9ed5d67759dac58ac246", size = 387292, upload-time = "2025-07-01T15:55:25.554Z" }, + { url = "https://files.pythonhosted.org/packages/7c/eb/3a9bb4bd90867d21916f253caf4f0d0be7098671b6715ad1cead9fe7bab9/rpds_py-0.26.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9a63785467b2d73635957d32a4f6e73d5e4df497a16a6392fa066b753e87387", size = 420435, upload-time = "2025-07-01T15:55:27.798Z" }, + { url = "https://files.pythonhosted.org/packages/cd/16/e066dcdb56f5632713445271a3f8d3d0b426d51ae9c0cca387799df58b02/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de4ed93a8c91debfd5a047be327b7cc8b0cc6afe32a716bbbc4aedca9e2a83af", size = 562410, upload-time = "2025-07-01T15:55:29.057Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/ddbdec7eb82a0dc2e455be44c97c71c232983e21349836ce9f272e8a3c29/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:caf51943715b12af827696ec395bfa68f090a4c1a1d2509eb4e2cb69abbbdb33", size = 590724, upload-time = "2025-07-01T15:55:30.719Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/95744085e65b7187d83f2fcb0bef70716a1ea0a9e5d8f7f39a86e5d83424/rpds_py-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4a59e5bc386de021f56337f757301b337d7ab58baa40174fb150accd480bc953", size = 558285, upload-time = "2025-07-01T15:55:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/6309a75e464d1da2559446f9c811aa4d16343cebe3dbb73701e63f760caa/rpds_py-0.26.0-cp314-cp314-win32.whl", hash = "sha256:92c8db839367ef16a662478f0a2fe13e15f2227da3c1430a782ad0f6ee009ec9", size = 223459, upload-time = "2025-07-01T15:55:33.312Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/8e9c11214c46098b1d1391b7e02b70bb689ab963db3b19540cba17315291/rpds_py-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:b0afb8cdd034150d4d9f53926226ed27ad15b7f465e93d7468caaf5eafae0d37", size = 236083, upload-time = "2025-07-01T15:55:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/47/af/9c4638994dd623d51c39892edd9d08e8be8220a4b7e874fa02c2d6e91955/rpds_py-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:ca3f059f4ba485d90c8dc75cb5ca897e15325e4e609812ce57f896607c1c0867", size = 223291, upload-time = "2025-07-01T15:55:36.202Z" }, + { url = "https://files.pythonhosted.org/packages/4d/db/669a241144460474aab03e254326b32c42def83eb23458a10d163cb9b5ce/rpds_py-0.26.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5afea17ab3a126006dc2f293b14ffc7ef3c85336cf451564a0515ed7648033da", size = 361445, upload-time = "2025-07-01T15:55:37.483Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/133f61cc5807c6c2fd086a46df0eb8f63a23f5df8306ff9f6d0fd168fecc/rpds_py-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69f0c0a3df7fd3a7eec50a00396104bb9a843ea6d45fcc31c2d5243446ffd7a7", size = 347206, upload-time = "2025-07-01T15:55:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/05/bf/0e8fb4c05f70273469eecf82f6ccf37248558526a45321644826555db31b/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:801a71f70f9813e82d2513c9a96532551fce1e278ec0c64610992c49c04c2dad", size = 380330, upload-time = "2025-07-01T15:55:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a8/060d24185d8b24d3923322f8d0ede16df4ade226a74e747b8c7c978e3dd3/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df52098cde6d5e02fa75c1f6244f07971773adb4a26625edd5c18fee906fa84d", size = 392254, upload-time = "2025-07-01T15:55:42.015Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7b/7c2e8a9ee3e6bc0bae26bf29f5219955ca2fbb761dca996a83f5d2f773fe/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bc596b30f86dc6f0929499c9e574601679d0341a0108c25b9b358a042f51bca", size = 516094, upload-time = "2025-07-01T15:55:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/75/d6/f61cafbed8ba1499b9af9f1777a2a199cd888f74a96133d8833ce5eaa9c5/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9dfbe56b299cf5875b68eb6f0ebaadc9cac520a1989cac0db0765abfb3709c19", size = 402889, upload-time = "2025-07-01T15:55:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/92/19/c8ac0a8a8df2dd30cdec27f69298a5c13e9029500d6d76718130f5e5be10/rpds_py-0.26.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac64f4b2bdb4ea622175c9ab7cf09444e412e22c0e02e906978b3b488af5fde8", size = 384301, upload-time = "2025-07-01T15:55:47.098Z" }, + { url = "https://files.pythonhosted.org/packages/41/e1/6b1859898bc292a9ce5776016c7312b672da00e25cec74d7beced1027286/rpds_py-0.26.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:181ef9b6bbf9845a264f9aa45c31836e9f3c1f13be565d0d010e964c661d1e2b", size = 412891, upload-time = "2025-07-01T15:55:48.412Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b9/ceb39af29913c07966a61367b3c08b4f71fad841e32c6b59a129d5974698/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:49028aa684c144ea502a8e847d23aed5e4c2ef7cadfa7d5eaafcb40864844b7a", size = 557044, upload-time = "2025-07-01T15:55:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/2f/27/35637b98380731a521f8ec4f3fd94e477964f04f6b2f8f7af8a2d889a4af/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e5d524d68a474a9688336045bbf76cb0def88549c1b2ad9dbfec1fb7cfbe9170", size = 585774, upload-time = "2025-07-01T15:55:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" }, + { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, ] [[package]] @@ -2411,45 +2637,54 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload_time = "2025-04-16T09:51:18.218Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] [[package]] @@ -2461,9 +2696,9 @@ dependencies = [ { name = "standard-aifc", marker = "python_full_version >= '3.13'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/7b/51d8b756aa1066b3f95bcbe3795f382f630ca9d2559ed808dada022141bf/speechrecognition-3.14.3.tar.gz", hash = "sha256:bdd2000a9897832b33095e33adfa48580787255706092e1346d1c6c36adae0a4", size = 32858109, upload_time = "2025-05-12T23:42:29.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/7b/51d8b756aa1066b3f95bcbe3795f382f630ca9d2559ed808dada022141bf/speechrecognition-3.14.3.tar.gz", hash = "sha256:bdd2000a9897832b33095e33adfa48580787255706092e1346d1c6c36adae0a4", size = 32858109, upload-time = "2025-05-12T23:42:29.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/cd/4b5f5d04c8a4e25c376858d0ad28c325f079f17c82bf379185abf45e41bf/speechrecognition-3.14.3-py3-none-any.whl", hash = "sha256:1859fbb09ae23fa759200f5b0677307f1fb16e2c5c798f4259fcc41dd5399fe6", size = 32853520, upload_time = "2025-05-12T23:42:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/4b5f5d04c8a4e25c376858d0ad28c325f079f17c82bf379185abf45e41bf/speechrecognition-3.14.3-py3-none-any.whl", hash = "sha256:1859fbb09ae23fa759200f5b0677307f1fb16e2c5c798f4259fcc41dd5399fe6", size = 32853520, upload-time = "2025-05-12T23:42:23.485Z" }, ] [[package]] @@ -2474,25 +2709,25 @@ dependencies = [ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload_time = "2025-05-14T17:10:32.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload_time = "2025-05-14T17:55:24.854Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload_time = "2025-05-14T17:55:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload_time = "2025-05-14T17:50:38.227Z" }, - { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload_time = "2025-05-14T17:51:49.829Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload_time = "2025-05-14T17:50:39.774Z" }, - { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload_time = "2025-05-14T17:51:51.736Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload_time = "2025-05-14T17:55:49.915Z" }, - { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload_time = "2025-05-14T17:55:51.349Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload_time = "2025-05-14T17:55:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload_time = "2025-05-14T17:55:34.921Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload_time = "2025-05-14T17:50:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload_time = "2025-05-14T17:51:54.722Z" }, - { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload_time = "2025-05-14T17:50:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload_time = "2025-05-14T17:51:57.308Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload_time = "2025-05-14T17:55:52.69Z" }, - { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload_time = "2025-05-14T17:55:54.495Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload_time = "2025-05-14T17:39:42.154Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, ] [[package]] @@ -2502,9 +2737,9 @@ 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, upload_time = "2025-05-30T13:34:12.914Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284, upload-time = "2025-05-30T13:34:12.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload_time = "2025-05-30T13:34:11.703Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606, upload-time = "2025-05-30T13:34:11.703Z" }, ] [[package]] @@ -2515,18 +2750,18 @@ dependencies = [ { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "standard-chunk", marker = "python_full_version >= '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload_time = "2024-10-30T16:01:31.772Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload_time = "2024-10-30T16:01:07.071Z" }, + { url = "https://files.pythonhosted.org/packages/c3/52/5fbb203394cc852334d1575cc020f6bcec768d2265355984dfd361968f36/standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66", size = 10492, upload-time = "2024-10-30T16:01:07.071Z" }, ] [[package]] name = "standard-chunk" version = "3.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload_time = "2024-10-30T16:18:28.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/06/ce1bb165c1f111c7d23a1ad17204d67224baa69725bb6857a264db61beaf/standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654", size = 4672, upload-time = "2024-10-30T16:18:28.326Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload_time = "2024-10-30T16:18:26.694Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/a5c1084d87767d787a6caba615aa50dc587229646308d9420c960cb5e4c0/standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c", size = 4944, upload-time = "2024-10-30T16:18:26.694Z" }, ] [[package]] @@ -2536,9 +2771,9 @@ 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, upload_time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload_time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, ] [[package]] @@ -2548,9 +2783,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload_time = "2025-04-27T18:05:01.611Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload_time = "2025-04-27T18:04:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] @@ -2562,18 +2797,18 @@ dependencies = [ { name = "requests" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/c1/5956e9711313a1bcaa3b6462b378014998ce394bd7cd6eb43a975d430bc7/tavily_python-0.7.9.tar.gz", hash = "sha256:61aa13ca89e2e40d645042c8d27afc478b27846fb79bb21d4f683ed28f173dc7", size = 19173, upload_time = "2025-07-01T22:44:01.759Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/c1/5956e9711313a1bcaa3b6462b378014998ce394bd7cd6eb43a975d430bc7/tavily_python-0.7.9.tar.gz", hash = "sha256:61aa13ca89e2e40d645042c8d27afc478b27846fb79bb21d4f683ed28f173dc7", size = 19173, upload-time = "2025-07-01T22:44:01.759Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/b4/14305cbf1e82ee51c74b1e1906ee70f4a2e62719dc8a8614f1fa562af376/tavily_python-0.7.9-py3-none-any.whl", hash = "sha256:6d70ea86e2ccba061d0ea98c81922784a01c186960304d44436304f114f22372", size = 15666, upload_time = "2025-07-01T22:43:59.25Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b4/14305cbf1e82ee51c74b1e1906ee70f4a2e62719dc8a8614f1fa562af376/tavily_python-0.7.9-py3-none-any.whl", hash = "sha256:6d70ea86e2ccba061d0ea98c81922784a01c186960304d44436304f114f22372", size = 15666, upload-time = "2025-07-01T22:43:59.25Z" }, ] [[package]] name = "tenacity" version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload_time = "2024-07-05T07:25:31.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload_time = "2024-07-05T07:25:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" }, ] [[package]] @@ -2584,20 +2819,20 @@ 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, upload_time = "2025-02-14T06:03:01.003Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } 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, upload_time = "2025-02-14T06:02:24.768Z" }, - { 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, upload_time = "2025-02-14T06:02:26.92Z" }, - { 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, upload_time = "2025-02-14T06:02:28.124Z" }, - { 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, upload_time = "2025-02-14T06:02:29.845Z" }, - { 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, upload_time = "2025-02-14T06:02:33.838Z" }, - { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload_time = "2025-02-14T06:02:36.265Z" }, - { 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, upload_time = "2025-02-14T06:02:37.494Z" }, - { 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, upload_time = "2025-02-14T06:02:39.516Z" }, - { 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, upload_time = "2025-02-14T06:02:41.791Z" }, - { 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, upload_time = "2025-02-14T06:02:43Z" }, - { 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, upload_time = "2025-02-14T06:02:45.046Z" }, - { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload_time = "2025-02-14T06:02:47.341Z" }, + { 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, upload-time = "2025-02-14T06:02:24.768Z" }, + { 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, upload-time = "2025-02-14T06:02:26.92Z" }, + { 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, upload-time = "2025-02-14T06:02:28.124Z" }, + { 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, upload-time = "2025-02-14T06:02:29.845Z" }, + { 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, upload-time = "2025-02-14T06:02:33.838Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" }, + { 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, upload-time = "2025-02-14T06:02:37.494Z" }, + { 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, upload-time = "2025-02-14T06:02:39.516Z" }, + { 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, upload-time = "2025-02-14T06:02:41.791Z" }, + { 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, upload-time = "2025-02-14T06:02:43Z" }, + { 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, upload-time = "2025-02-14T06:02:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" }, ] [[package]] @@ -2607,22 +2842,22 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload_time = "2025-03-13T10:51:18.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload_time = "2025-03-13T10:51:09.459Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload_time = "2025-03-13T10:51:07.692Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload_time = "2025-03-13T10:50:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload_time = "2025-03-13T10:50:59.525Z" }, - { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload_time = "2025-03-13T10:51:04.678Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload_time = "2025-03-13T10:51:01.261Z" }, - { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload_time = "2025-03-13T10:51:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload_time = "2025-03-13T10:51:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload_time = "2025-03-13T10:51:10.927Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload_time = "2025-03-13T10:51:12.688Z" }, - { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload_time = "2025-03-13T10:51:14.723Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload_time = "2025-03-13T10:51:16.526Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload_time = "2025-03-13T10:51:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload_time = "2025-03-13T10:51:19.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" }, + { url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" }, ] [[package]] @@ -2632,9 +2867,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload_time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload_time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] @@ -2647,18 +2882,18 @@ dependencies = [ { 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, upload_time = "2025-05-26T14:30:31.824Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload_time = "2025-05-26T14:30:30.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, ] [[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, upload_time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload_time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ] [[package]] @@ -2669,9 +2904,9 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload_time = "2023-05-24T20:25:47.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload_time = "2023-05-24T20:25:45.287Z" }, + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, ] [[package]] @@ -2681,27 +2916,27 @@ 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, upload_time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload_time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] [[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, upload_time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload_time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload_time = "2025-06-18T14:07:41.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload_time = "2025-06-18T14:07:40.39Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -2712,82 +2947,91 @@ dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload_time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload_time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload_time = "2024-11-01T14:07:13.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload_time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload_time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload_time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload_time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload_time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload_time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload_time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload_time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload_time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload_time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload_time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload_time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload_time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload_time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload_time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload_time = "2024-11-01T14:07:11.845Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[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, upload_time = "2025-03-05T20:03:41.606Z" } -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, upload_time = "2025-03-05T20:02:16.706Z" }, - { 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, upload_time = "2025-03-05T20:02:18.832Z" }, - { 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, upload_time = "2025-03-05T20:02:20.187Z" }, - { 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, upload_time = "2025-03-05T20:02:22.286Z" }, - { 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, upload_time = "2025-03-05T20:02:24.368Z" }, - { 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, upload_time = "2025-03-05T20:02:25.669Z" }, - { 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, upload_time = "2025-03-05T20:02:26.99Z" }, - { 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, upload_time = "2025-03-05T20:02:30.291Z" }, - { 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, upload_time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload_time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload_time = "2025-03-05T20:02:34.498Z" }, - { 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, upload_time = "2025-03-05T20:02:36.695Z" }, - { 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, upload_time = "2025-03-05T20:02:37.985Z" }, - { 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, upload_time = "2025-03-05T20:02:39.298Z" }, - { 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, upload_time = "2025-03-05T20:02:40.595Z" }, - { 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, upload_time = "2025-03-05T20:02:41.926Z" }, - { 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, upload_time = "2025-03-05T20:02:43.304Z" }, - { 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, upload_time = "2025-03-05T20:02:48.812Z" }, - { 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, upload_time = "2025-03-05T20:02:50.14Z" }, - { 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, upload_time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload_time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload_time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload_time = "2025-03-05T20:03:39.41Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +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, upload-time = "2025-03-05T20:02:16.706Z" }, + { 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, upload-time = "2025-03-05T20:02:18.832Z" }, + { 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, upload-time = "2025-03-05T20:02:20.187Z" }, + { 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, upload-time = "2025-03-05T20:02:22.286Z" }, + { 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, upload-time = "2025-03-05T20:02:24.368Z" }, + { 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, upload-time = "2025-03-05T20:02:25.669Z" }, + { 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, upload-time = "2025-03-05T20:02:26.99Z" }, + { 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, upload-time = "2025-03-05T20:02:30.291Z" }, + { 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, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { 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, upload-time = "2025-03-05T20:02:36.695Z" }, + { 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, upload-time = "2025-03-05T20:02:37.985Z" }, + { 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, upload-time = "2025-03-05T20:02:39.298Z" }, + { 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, upload-time = "2025-03-05T20:02:40.595Z" }, + { 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, upload-time = "2025-03-05T20:02:41.926Z" }, + { 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, upload-time = "2025-03-05T20:02:43.304Z" }, + { 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, upload-time = "2025-03-05T20:02:48.812Z" }, + { 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, upload-time = "2025-03-05T20:02:50.14Z" }, + { 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, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, ] [[package]] name = "xlrd" version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload_time = "2025-06-14T08:46:39.039Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d1990d7af382c79f3b2372e880b65de21b01b1a2b78691e/xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9", size = 100167, upload-time = "2025-06-14T08:46:39.039Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload_time = "2025-06-14T08:46:37.766Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, ] [[package]] name = "xlsxwriter" version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload_time = "2025-06-17T08:59:14.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/47/7704bac42ac6fe1710ae099b70e6a1e68ed173ef14792b647808c357da43/xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe", size = 213306, upload-time = "2025-06-17T08:59:14.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload_time = "2025-06-17T08:59:13.453Z" }, + { url = "https://files.pythonhosted.org/packages/fa/34/a22e6664211f0c8879521328000bdcae9bf6dbafa94a923e531f6d5b3f73/xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd", size = 172347, upload-time = "2025-06-17T08:59:13.453Z" }, ] [[package]] @@ -2799,60 +3043,60 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload_time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload_time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload_time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload_time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload_time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload_time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload_time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload_time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload_time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload_time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload_time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload_time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload_time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload_time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload_time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload_time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload_time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload_time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload_time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload_time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload_time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload_time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload_time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload_time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload_time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload_time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload_time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload_time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload_time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload_time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload_time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload_time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload_time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload_time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload_time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload_time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload_time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload_time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload_time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload_time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload_time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload_time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload_time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload_time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload_time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload_time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload_time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload_time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload_time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload_time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload_time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload_time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload_time = "2025-06-10T00:46:07.521Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] [[package]] @@ -2863,27 +3107,27 @@ dependencies = [ { name = "defusedxml" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload_time = "2025-03-25T18:14:21.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/32/f60d87a99c05a53604c58f20f670c7ea6262b55e0bbeb836ffe4550b248b/youtube_transcript_api-1.0.3.tar.gz", hash = "sha256:902baf90e7840a42e1e148335e09fe5575dbff64c81414957aea7038e8a4db46", size = 2153252, upload-time = "2025-03-25T18:14:21.119Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload_time = "2025-03-25T18:14:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/f0/44/40c03bb0f8bddfb9d2beff2ed31641f52d96c287ba881d20e0c074784ac2/youtube_transcript_api-1.0.3-py3-none-any.whl", hash = "sha256:d1874e57de65cf14c9d7d09b2b37c814d6287fa0e770d4922c4cd32a5b3f6c47", size = 2169911, upload-time = "2025-03-25T18:14:19.416Z" }, ] [[package]] name = "yt-dlp" version = "2025.6.30" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/9c/ff64c2fed7909f43a9a0aedb7395c65404e71c2439198764685a6e3b3059/yt_dlp-2025.6.30.tar.gz", hash = "sha256:6d0ae855c0a55bfcc28dffba804ec8525b9b955d34a41191a1561a4cec03d8bd", size = 3034364, upload_time = "2025-06-30T23:58:36.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/9c/ff64c2fed7909f43a9a0aedb7395c65404e71c2439198764685a6e3b3059/yt_dlp-2025.6.30.tar.gz", hash = "sha256:6d0ae855c0a55bfcc28dffba804ec8525b9b955d34a41191a1561a4cec03d8bd", size = 3034364, upload-time = "2025-06-30T23:58:36.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/41/2f048ae3f6d0fa2e59223f08ba5049dbcdac628b0a9f9deac722dd9260a5/yt_dlp-2025.6.30-py3-none-any.whl", hash = "sha256:541becc29ed7b7b3a08751c0a66da4b7f8ee95cb81066221c78e83598bc3d1f3", size = 3279333, upload_time = "2025-06-30T23:58:34.911Z" }, + { url = "https://files.pythonhosted.org/packages/14/41/2f048ae3f6d0fa2e59223f08ba5049dbcdac628b0a9f9deac722dd9260a5/yt_dlp-2025.6.30-py3-none-any.whl", hash = "sha256:541becc29ed7b7b3a08751c0a66da4b7f8ee95cb81066221c78e83598bc3d1f3", size = 3279333, upload-time = "2025-06-30T23:58:34.911Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload_time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload_time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] [[package]] @@ -2893,38 +3137,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload_time = "2024-07-15T00:18:06.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload_time = "2024-07-15T00:15:35.815Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload_time = "2024-07-15T00:15:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload_time = "2024-07-15T00:15:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload_time = "2024-07-15T00:15:41.75Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload_time = "2024-07-15T00:15:44.114Z" }, - { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload_time = "2024-07-15T00:15:46.509Z" }, - { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload_time = "2024-07-15T00:15:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload_time = "2024-07-15T00:15:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload_time = "2024-07-15T00:15:54.971Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload_time = "2024-07-15T00:15:57.634Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload_time = "2024-07-15T00:16:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload_time = "2024-07-15T00:16:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload_time = "2024-07-15T00:16:06.694Z" }, - { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload_time = "2024-07-15T00:16:09.758Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload_time = "2024-07-15T00:16:11.758Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload_time = "2024-07-15T00:16:13.731Z" }, - { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload_time = "2024-07-15T00:16:16.005Z" }, - { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload_time = "2024-07-15T00:16:17.897Z" }, - { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload_time = "2024-07-15T00:16:20.136Z" }, - { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload_time = "2024-07-15T00:16:23.398Z" }, - { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload_time = "2024-07-15T00:16:26.391Z" }, - { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload_time = "2024-07-15T00:16:29.018Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload_time = "2024-07-15T00:16:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload_time = "2024-07-15T00:16:34.593Z" }, - { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload_time = "2024-07-15T00:16:36.887Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload_time = "2024-07-15T00:16:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload_time = "2024-07-15T00:16:41.83Z" }, - { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload_time = "2024-07-15T00:16:44.287Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload_time = "2024-07-15T00:16:46.423Z" }, - { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload_time = "2024-07-15T00:16:49.053Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload_time = "2024-07-15T00:16:51.003Z" }, - { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload_time = "2024-07-15T00:16:53.135Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, + { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, + { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, + { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, + { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, + { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, + { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, ] diff --git a/docs/architecture.md b/docs/architecture.md index e186538..59d6691 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -126,3 +126,14 @@ This section describes the step-by-step lifecycle of a typical research task, li 1. `{run_id}.json`: A complete, serializable snapshot of the entire `RunContext`, saved periodically. 2. `{run_name}.iic`: A lightweight file containing core metadata (ID, name, creation date) for quick browsing and identification. A `project.iic` file in each project's root directory serves as a fast-lookup index for all runs within it. +### 4.8 Budget-Aware Content Inheritance +* **Location**: `utils/content_selection.py`, `nodes/custom_nodes/dispatcher_node.py` +* **Problem**: When Associates inherit context from completed work modules via `inherit_messages_from`, unbounded message injection can cause new agents to exceed their context budget at birth. +* **Mechanism**: A two-tier content selection strategy enforces budget limits: + 1. **Budget Computation**: `(target_context_limit Γ— 0.40) Γ· num_sources` allocates fair share per source module. + 2. **Tier 1 Selection**: Use `deliverables.primary_summary` (LLM-generated summary from finish_flow) if it exists and fits budget. + 3. **Tier 2 Selection**: Fall back to newest-first message selection with **hydration before measurement** to ensure accurate sizing. +* **Key Design Decision**: Knowledge Base tokens in archived messages are expanded (hydrated) BEFORE size measurement, not after. This prevents post-selection expansion from exceeding budget limits. +* **Integration Point**: Content selection happens in `dispatcher_node._preselect_inherited_content()` BEFORE the HandoverService is called, ensuring budget compliance at dispatch time. + +See [Context Budget Management Architecture](architecture/context-budget-management.md) for detailed design documentation. \ No newline at end of file diff --git a/docs/architecture/context-budget-management.md b/docs/architecture/context-budget-management.md new file mode 100644 index 0000000..450f08d --- /dev/null +++ b/docs/architecture/context-budget-management.md @@ -0,0 +1,528 @@ +# Context Budget Management Architecture + +## Problem Statement + +The CommonGround multi-agent system experiences context window explosions that trigger circuit breakers, resulting in incomplete or failed responses. Analysis of session `orange-seagull-of-teaching` revealed: + +- **Principal Token Count**: 211,901 tokens (106% of 200K limit) +- **Partner Token Count**: 241,967 tokens (121% of 200K limit) +- **Root Cause**: Full context archives (~121K chars) injected via `work_modules_ingestor` +- **Secondary Issue**: Tool definitions duplicated in messages (~40-50K tokens) + +## Design Principles + +1. **Information Flows Up as Summaries**: Subagent work products flow up to Principal/Partner as compressed deliverables, not full context +2. **Detail Preserved at Source**: Full context archives remain available for drill-down but are never injected by default +3. **Budget Allocation is Hierarchical**: Each level reserves capacity for summarization before delegating +4. **Graceful Degradation**: When limits approach, synthesize partial results rather than fail completely +5. **1M Context as Safety Net**: Extended context (1M tokens) provides headroom, not permission to be wasteful + +## Architecture Overview + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CONTEXT BUDGET HIERARCHY β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PARTNER (1M tokens) β”‚ β”‚ +β”‚ β”‚ β”œβ”€ System Prompt: ~20K β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Tool Definitions: ~50K β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Summarization Reserve: 300K (30%) β”‚ β”‚ +β”‚ β”‚ └─ Working Budget: 630K β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PRINCIPAL (1M tokens) β”‚ β”‚ +β”‚ β”‚ β”œβ”€ System Prompt: ~20K β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Tool Definitions: ~50K β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Work Module Summaries: variable (target <50K) β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Summarization Reserve: 300K (30%) β”‚ β”‚ +β”‚ β”‚ └─ Working Budget: 580K β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β–Ό β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Associate β”‚ β”‚ Associate β”‚ β”‚ Associate β”‚ (200K each default) β”‚ +β”‚ β”‚ Module A β”‚ β”‚ Module B β”‚ β”‚ Module C β”‚ β”‚ +β”‚ β”‚ β”œβ”€ Budget β”‚ β”‚ β”œβ”€ Budget β”‚ β”‚ β”œβ”€ Budget β”‚ Per-worker: 119K β”‚ +β”‚ β”‚ └─ Reserve β”‚ β”‚ └─ Reserve β”‚ β”‚ └─ Reserve β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Component Fixes + +### 1. Work Modules Ingestor (CRITICAL) + +**Problem**: Injects full `context_archive` when formatting work modules status. + +**Solution**: Filter out `context_archive` and large fields, show only summary metadata. + +**File**: `core/agent_core/events/ingestors.py` + +```python +@register_ingestor("work_modules_ingestor") +def work_modules_ingestor(payload: Any, params: Dict, context: Dict) -> str: + """ + Formats work_modules dictionary as Markdown. + + IMPORTANT: Only includes summary fields. Full context_archive is stored + but never injected into prompts. Use dispatch_result_ingestor for deliverables. + """ + if not isinstance(payload, dict): + return "Work modules data is not in the expected format (dictionary)." + + # Fields to EXCLUDE from injection (available for drill-down only) + EXCLUDED_FIELDS = { + 'context_archive', # Full message history - too large + 'full_context', # Any full context dump + 'raw_messages', # Raw message lists + 'messages', # Message arrays + } + + # Fields to SUMMARIZE (show counts/metadata only) + SUMMARIZE_FIELDS = { + 'deliverables': lambda v: f"[{len(v) if isinstance(v, (list, dict)) else 0} items]", + 'tools_used': lambda v: f"[{len(v) if isinstance(v, list) else 0} tools]", + } + + def _filter_module(module_data: dict) -> dict: + """Filter a single work module to exclude large fields.""" + filtered = {} + for key, value in module_data.items(): + if key in EXCLUDED_FIELDS: + continue + if key in SUMMARIZE_FIELDS: + filtered[key] = SUMMARIZE_FIELDS[key](value) + else: + filtered[key] = value + return filtered + + lines = [params.get("title", "### Current Work Modules Status")] + if not payload: + lines.append("No work modules are currently defined.") + else: + # Filter each module before formatting + filtered_modules = { + mod_id: _filter_module(mod_data) if isinstance(mod_data, dict) else mod_data + for mod_id, mod_data in payload.items() + } + formatted_modules = _recursive_markdown_formatter(filtered_modules, {}, level=0) + lines.extend(formatted_modules) + + return "\n".join(lines) +``` + +### 2. Context Budget Guardian Updates + +**File**: `core/agent_core/framework/context_budget_guardian.py` + +The guardian already supports 1M context detection. Key behaviors: + +1. **Priority Resolution**: + - First: Check explicit `max_context_tokens` in config + - Second: Check `extra_headers` for `anthropic-beta: context-1m-*` + - Third: Model family defaults + - Fourth: Conservative 100K default + +2. **Thresholds** (already implemented): + - WARNING: 40% - Start suggesting wrap-up + - CRITICAL: 55% - Force completion + - EXCEEDED: 70% - Circuit breaker, 30% remains for wrap-up + +### 2.1 Provider-Aware Token Counting + +**File**: `core/agent_core/llm/token_counter.py` + +**Problem**: LiteLLM's `token_counter()` uses tiktoken (OpenAI's tokenizer) as fallback for Claude 3+ models, which underestimates token counts by 25-35%. This caused the Context Budget Guardian to believe it had more headroom than actually available, leading to context window overflows. + +**Solution**: Provider-aware token counting that uses official provider APIs when available: + +- **Anthropic (Claude)**: Uses `client.messages.count_tokens()` - free, accurate, separate rate limits +- **OpenAI (GPT)**: Uses litellm/tiktoken (accurate for OpenAI models) +- **Google (Gemini)**: Falls back to litellm (future: use countTokens API) +- **Unknown providers**: Falls back to litellm estimation + +**Architecture**: +```python +# Provider detection via model name patterns +LLMProvider = Enum("LLMProvider", ["ANTHROPIC", "OPENAI", "GOOGLE", "UNKNOWN"]) + +# Registry of provider-specific counters +PROVIDER_TOKEN_COUNTERS = { + LLMProvider.ANTHROPIC: _count_tokens_anthropic, # Uses official API + LLMProvider.OPENAI: _count_tokens_openai, # Returns None (litellm accurate) + LLMProvider.GOOGLE: _count_tokens_google, # Placeholder for future +} +``` + +**Integration**: `estimate_prompt_tokens()` in `call_llm.py` delegates to this module, maintaining backward compatibility while providing accurate counts for Claude models. + +### 3. Circuit Breaker Graceful Degradation + +**Problem**: When circuit breaker fires, it returns nothing useful. + +**Solution**: Synthesize partial results from available work. + +**File**: `core/agent_core/framework/context_budget_guardian.py` (new function) + +```python +def synthesize_partial_results( + team_state: Dict, + triggered_agent: str, + budget_metadata: Dict +) -> Dict: + """ + When circuit breaker fires, compile available work into actionable summary. + + Returns a structured report of: + - What was requested + - What was completed + - What remains incomplete + - Key findings so far + """ + work_modules = team_state.get("work_modules", {}) + + completed_modules = [] + incomplete_modules = [] + partial_deliverables = [] + + for module_id, module in work_modules.items(): + status = module.get("status", "unknown") + if status in ("completed", "done"): + completed_modules.append({ + "id": module_id, + "objective": module.get("objective", "Unknown"), + "deliverables": module.get("deliverables", {}) + }) + if module.get("deliverables"): + partial_deliverables.extend( + _extract_key_findings(module["deliverables"]) + ) + else: + incomplete_modules.append({ + "id": module_id, + "objective": module.get("objective", "Unknown"), + "status": status, + "assigned_to": module.get("assigned_agent_id") + }) + + return { + "circuit_breaker_synthesis": True, + "triggered_by": triggered_agent, + "budget_at_trigger": budget_metadata, + "summary": { + "total_modules": len(work_modules), + "completed": len(completed_modules), + "incomplete": len(incomplete_modules) + }, + "completed_work": completed_modules, + "incomplete_work": incomplete_modules, + "key_findings_so_far": partial_deliverables, + "recommendation": ( + f"Context budget exceeded ({budget_metadata.get('utilization_percent', 0):.1f}% used). " + f"Returning {len(completed_modules)} completed modules. " + f"{len(incomplete_modules)} modules remain incomplete and can be resumed." + ) + } +``` + +### 4. Deliverable Capture Enforcement + +**Problem**: Subagents completing with 0 deliverables. + +**Solution**: Enforce deliverable registration through FIM prompts. + +**File**: `core/agent_profiles/profiles/Associate_*_EN.yaml` (all associate profiles) + +Add to `fim_protocol`: + +```yaml +fim_protocol: + trigger_conditions: + budget_threshold_percent: 70 + max_turns_without_fim: 5 + + mandatory_deliverable_check: + enabled: true + on_completion: | + Before signaling completion, verify: + 1. You have registered at least one deliverable using `register_deliverable` + 2. The deliverable contains actionable findings, not just "task attempted" + 3. If you cannot produce a meaningful deliverable, explain why in the deliverable +``` + +### 5. LLM Config Update (COMPLETED) + +**File**: `core/agent_profiles/llm_configs/principal_llm.yaml` + +```yaml +config: + api_key: + _type: "from_env" + var: "ANTHROPIC_API_KEY" + required: true + model: "anthropic/claude-sonnet-4-5-20250929" + temperature: + _type: "from_env" + var: "PRINCIPAL_TEMPERATURE" + required: false + default: 0.4 + extra_headers: + _type: "from_env" + var: "ANTHROPIC_EXTRA_HEADERS" + required: false + default: null + max_context_tokens: + _type: "from_env" + var: "PRINCIPAL_MAX_CONTEXT_TOKENS" + required: false + default: 1000000 # 1M default for models that support it +``` + +## Environment Configuration + +**File**: `core/.env` + +```bash +# 1M Context Configuration +ANTHROPIC_EXTRA_HEADERS={"anthropic-beta": "context-1m-2025-08-07"} + +# Optional: Override default max context tokens +# PRINCIPAL_MAX_CONTEXT_TOKENS=1000000 +``` + +## Token Budget Guidelines + +### For Principal Agent + +| Component | Target Budget | Notes | +|-----------|--------------|-------| +| System Prompt | 15-20K | Core instructions, role definition | +| Tool Definitions | 40-50K | Can be reduced by trimming docstrings | +| Work Module Summaries | 30-50K | Filtered view, no context_archive | +| Active Conversation | 200-400K | Working memory | +| Summarization Reserve | 300K | For final synthesis | +| **Total Available** | **1M** | With 1M context enabled | + +### For Associate Agents + +| Component | Target Budget | Notes | +|-----------|--------------|-------| +| System Prompt | 10-15K | Role + briefing | +| Tool Definitions | 20-30K | Subset of Principal's tools | +| Work Context | 50-100K | Task-specific context | +| Working Memory | 50-100K | Conversation history | +| **Total Available** | **200K** | Standard context | + +## Implementation Priority + +1. **[CRITICAL] Fix work_modules_ingestor** - Immediate token reduction +2. **[HIGH] Verify 1M context** - Safety net (DONE βœ“) +3. **[HIGH] Graceful circuit breaker** - Better failure recovery +4. **[MEDIUM] Deliverable enforcement** - Data quality +5. **[LOW] Tool docstring optimization** - Long-term token savings + +## Monitoring & Alerts + +The system should log warnings when: + +1. Single work module exceeds 10K tokens when serialized +2. Total work_modules payload exceeds 50K tokens +3. Context utilization exceeds WARNING threshold (40%) +4. Any agent completes with 0 deliverables +5. Circuit breaker fires (should be exceptional, not routine) + +## Testing Checklist + +- [ ] Verify 1M context enabled: `extra_headers` resolved correctly +- [ ] Verify `max_context_tokens: 1000000` in resolved config +- [ ] Verify work_modules_ingestor excludes context_archive +- [ ] Test circuit breaker synthesizes partial results +- [ ] Confirm deliverable capture enforcement works +- [ ] Load test with complex multi-module scenario +- [ ] Verify inheritance budget computation works correctly +- [ ] Test content selection uses LLM summary when available +- [ ] Confirm hydration occurs before selection (not after) + +--- + +## 6. Content Inheritance Budget Management + +### 6.1 Problem Statement + +When Associates are spawned with `inherit_messages_from` parameter, unbounded raw message history can be injected into the new agent's briefing, causing context explosion at birth. + +**Observed Failure (Session: `independent-saffron-kittiwake`)**: +- E_1 (no inheritance): Born at 983 tokens (0.5% budget) +- E_6 (inherits from WM_2, WM_3): Born at 171,947 tokens (86% budget) +- Root Cause: 60KB of raw messages inherited without budget limits +- Result: Infinite loop as circuit breaker triggered immediately + +### 6.2 Architecture: Content Inheritance Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CONTENT INHERITANCE DATA FLOW (FIXED) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ [Source Module Finishes - e.g., WM_2] β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ finish_flow() or generate_message_summary() called β”‚ +β”‚ β–Ό β”‚ +β”‚ context_archive[-1] = { β”‚ +β”‚ "messages": [...raw history with KB tokens...], β”‚ +β”‚ "deliverables": {"primary_summary": "...LLM-generated summary..."} β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ [Principal dispatches E_6 with inherit_messages_from: [WM_2, WM_3]] β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ dispatcher_node._preselect_inherited_content() ◄── NEW β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ 1. Compute target's context limit (e.g., 200K tokens) β”‚ +β”‚ β”‚ 2. Reserve INHERITANCE_BUDGET_FRACTION (40%) for inheritance β”‚ +β”‚ β”‚ 3. Divide pool among sources: 200K * 0.40 / 2 = 40K tokens each β”‚ +β”‚ β”‚ 4. Convert to chars: 40K * 4 = 160K chars per source β”‚ +β”‚ β”‚ β”‚ +β”‚ β”‚ For each source module: β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ TIER 1: Try deliverables.primary_summary β”‚ β”‚ +β”‚ β”‚ β”‚ - If exists AND fits budget β†’ USE IT (no KB tokens!) β”‚ β”‚ +β”‚ β”‚ β”‚ - Skip to next source β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ +β”‚ β”‚ β”‚ TIER 2: Fall back to messages β”‚ β”‚ +β”‚ β”‚ β”‚ - HYDRATE messages first (expand KB tokens) β”‚ β”‚ +β”‚ β”‚ β”‚ - Select newest-first until budget exhausted β”‚ β”‚ +β”‚ β”‚ β”‚ - Never truncate individual messages β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ Pre-selected content stored in assignment parameters β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ HandoverService.execute() uses pre-selected content (via condition) β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ E_6 InboxProcessor renders budget-limited inherited content β”‚ +β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ +β”‚ E_6 born within budget βœ“ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 6.3 Key Design Decisions + +#### 6.3.1 Two-Tier Content Selection Strategy + +| Tier | Content Source | When Used | Pros | Cons | +|------|---------------|-----------|------|------| +| **1** | `deliverables.primary_summary` | When exists and fits budget | Clean, no KB tokens, LLM-quality summary | May lose granular detail | +| **2** | Raw messages (newest-first) | When no summary or summary too large | Preserves recent context | Requires hydration first | + +#### 6.3.2 Hydration Before Selection (Critical) + +Messages in `context_archive` may contain Knowledge Base tokens (e.g., `<#CGKB-00042>`). These tokens are compact placeholders that expand when hydrated. + +**Problem**: If we select messages by dehydrated size, then hydration happens later (in `base_agent_node._hydrate_messages()`), the final size could exceed budget. + +**Solution**: When using Tier 2 (message selection), we MUST: +1. Hydrate messages from KB **before** measuring size +2. Select from hydrated messages to get accurate sizing +3. Selected content is already hydrated β†’ no double-expansion + +#### 6.3.3 Budget Computation Formula + +```python +# Constants (elevated to module level in content_selection.py) +CHARS_PER_TOKEN = 4 +INHERITANCE_BUDGET_FRACTION = 0.40 # 40% of target's context + +# Computation +target_context_tokens = get_model_context_limit(target_model, llm_config) +pool_tokens = target_context_tokens * INHERITANCE_BUDGET_FRACTION +pool_chars = pool_tokens * CHARS_PER_TOKEN +per_source_budget_chars = pool_chars // num_sources +``` + +**Example**: +- Target model: claude-sonnet-4 (200K tokens) +- Inheritance pool: 200K * 0.40 = 80K tokens +- Sources: 2 modules (WM_2, WM_3) +- Per-source: 80K / 2 = 40K tokens = 160K chars each + +### 6.4 Implementation Files + +| File | Change Type | Purpose | +|------|-------------|---------| +| `agent_core/utils/content_selection.py` | **CREATE** | Shared utilities for budget-aware content selection | +| `agent_core/nodes/custom_nodes/dispatcher_node.py` | **MODIFY** | Add `_preselect_inherited_content()` method | +| `agent_profiles/handover_protocols/principal_to_associate_briefing.yaml` | **MODIFY** | Add conditional rule for pre-selected content | + +### 6.5 Content Selection Module API + +**File**: `agent_core/utils/content_selection.py` + +```python +# Constants +CHARS_PER_TOKEN = 4 +INHERITANCE_BUDGET_FRACTION = 0.40 +STRATEGY_LLM_SUMMARY = "llm_summary" +STRATEGY_NEWEST_FIRST = "newest_first" + +# Core Functions +def compute_inheritance_budget_chars( + target_context_limit_tokens: int, + num_sources: int, + inheritance_fraction: float = INHERITANCE_BUDGET_FRACTION +) -> int: + """Compute per-source character budget for content inheritance.""" + +def select_content_within_budget( + deliverables: Optional[Dict], + messages: List[Dict], + budget_chars: int, + source_id: str = "unknown" +) -> Tuple[Union[str, List[Dict]], Dict[str, Any]]: + """ + Two-tier content selection within a character budget. + + Returns: + Tuple of (selected_content, metadata) + - selected_content: Either summary string OR list of selected messages + - metadata: Dict with "strategy", "chars_used", "items_selected", etc. + """ + +def format_inherited_content_for_briefing( + content: Union[str, List[Dict]], + metadata: Dict[str, Any], + source_id: str +) -> List[Dict]: + """Format selected content as messages for injection into briefing.""" +``` + +### 6.6 Backward Compatibility + +The design maintains full backward compatibility: + +1. **No inheritance**: If `inherit_messages_from` is empty/absent, no changes to existing flow +2. **Protocol conditions**: New YAML rules use conditions to prefer pre-selected content when available +3. **Fallback path**: If pre-selection fails, original message iteration still works (with existing size issues) + +### 6.7 Monitoring & Logging + +New log events for observability: + +| Log Event | Level | When | +|-----------|-------|------| +| `inheritance_budget_computed` | DEBUG | Budget calculation completed | +| `inheritance_content_selection_started` | INFO | Starting content selection | +| `content_selection_using_summary` | DEBUG | Tier 1 path taken | +| `content_selection_summary_exceeds_budget` | DEBUG | Falling back to Tier 2 | +| `content_selection_newest_first_complete` | DEBUG | Tier 2 selection done | +| `inheritance_content_selected` | INFO | Per-source selection result | diff --git a/docs/architecture/session-resilience.md b/docs/architecture/session-resilience.md new file mode 100644 index 0000000..ae7e534 --- /dev/null +++ b/docs/architecture/session-resilience.md @@ -0,0 +1,489 @@ +# Session Resilience Architecture + +## Overview + +This document describes the WebSocket session resilience architecture for CommonGround, providing: +- **JWT-based session authentication** with HttpOnly cookie fingerprint binding +- **Dual heartbeat mechanism** (server-initiated ping + client-initiated heartbeat) +- **Reconnection grace period** for surviving temporary disconnects +- **Event buffering** for replay on reconnection +- **Auto-refresh tokens** at 90% of JWT lifespan + +## Design Goals + +1. **Session survives browser refresh** - User doesn't lose work when refreshing +2. **Quick reconnection** - Detect and recover from transient network issues fast +3. **Security** - JWT + fingerprint prevents token theft/replay +4. **Standards compliance** - OWASP JWT guidelines, RFC 8725 + +--- + +## Architecture Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SESSION RESILIENCE ARCHITECTURE β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ BROWSER β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ sessionStorage β”‚ β”‚ HttpOnly Cookie β”‚ β”‚ SessionManager β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ (Hardened) β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ JWT Token β”‚ β”‚ β”‚ β”‚ β€’ Auto-refresh β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Refresh Token β”‚ β”‚ β€’ Fingerprint β”‚ β”‚ at 90% β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Session ID β”‚ β”‚ (random str) β”‚ β”‚ β€’ Heartbeat β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Run ID β”‚ β”‚ β”‚ β”‚ sender β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ Last Event ID β”‚ β”‚ (NOT accessible β”‚ β”‚ β€’ Reconnection β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ to JavaScript) β”‚ β”‚ handler β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ WebSocket + JWT Header + Cookie β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ SERVER β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ SessionSecurityManager β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ create_session_tokens(session_id, project_id) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ validate_jwt_with_fingerprint(jwt, cookie) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ refresh_tokens(refresh_token, fingerprint) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ revoke_session(session_id) β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ β”‚ ConnectionManager (existing) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ register_connection() / unregister_connection() β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ reconnect_run() ← Now wired up β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ start_heartbeat() (server β†’ client) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ handle_client_heartbeat() ← NEW β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ buffer_event() / replay_buffered_events() β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β€’ grace_period_monitor() β”‚ β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Dual Heartbeat Mechanism + +Both server-initiated and client-initiated heartbeats operate concurrently for maximum resilience: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ DUAL HEARTBEAT MECHANISM β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ SERVER-INITIATED (Existing - Enhanced) β”‚ +β”‚ ─────────────────────────────────────── β”‚ +β”‚ β”‚ +β”‚ Purpose: Detect client disappearance β†’ trigger grace period β”‚ +β”‚ Interval: Every 30 seconds β”‚ +β”‚ Timeout: 10 seconds for pong response β”‚ +β”‚ Max missed: 3 before declaring client dead β”‚ +β”‚ β”‚ +β”‚ Server ───── { type: "ping", timestamp: "..." } ────► Client β”‚ +β”‚ ◄──── { type: "pong", timestamp: "...", ───── Client β”‚ +β”‚ lastEventId: 42, β”‚ +β”‚ clientTime: 1703779200000 } β”‚ +β”‚ β”‚ +β”‚ CLIENT-INITIATED (New) β”‚ +β”‚ ────────────────────── β”‚ +β”‚ β”‚ +β”‚ Purpose: Detect server unresponsiveness β†’ trigger reconnection attempt β”‚ +β”‚ Interval: Every 20 seconds β”‚ +β”‚ Timeout: 10 seconds for ack response β”‚ +β”‚ Max missed: 2 before attempting reconnection β”‚ +β”‚ β”‚ +β”‚ Client ───── { type: "heartbeat", ────► Server β”‚ +β”‚ timestamp: 1703779200000, β”‚ +β”‚ sessionId: "...", β”‚ +β”‚ runId: "..." } β”‚ +β”‚ ◄──── { type: "heartbeat_ack", ───── Server β”‚ +β”‚ timestamp: 1703779200000, β”‚ +β”‚ serverTime: "...", β”‚ +β”‚ sessionValid: true } β”‚ +β”‚ β”‚ +β”‚ FAILURE DETECTION β”‚ +β”‚ ───────────────── β”‚ +β”‚ β”‚ +β”‚ β”‚ Failure Scenario β”‚ Server Ping β”‚ Client HB β”‚ Detection β”‚ β”‚ +β”‚ │───────────────────────────────│─────────────│───────────│───────────│ β”‚ +β”‚ β”‚ Client browser closed β”‚ βœ… β”‚ ❌ β”‚ Fast β”‚ β”‚ +β”‚ β”‚ Client tab frozen β”‚ βœ… β”‚ ❌ β”‚ Fast β”‚ β”‚ +β”‚ β”‚ Client network dropped β”‚ βœ… β”‚ βœ… β”‚ Fast β”‚ β”‚ +β”‚ β”‚ Server process died β”‚ ❌ β”‚ βœ… β”‚ Fast β”‚ β”‚ +β”‚ β”‚ Server overloaded β”‚ ❌ β”‚ βœ… β”‚ Fast β”‚ β”‚ +β”‚ β”‚ Network partition β”‚ βœ… β”‚ βœ… β”‚ Fast β”‚ β”‚ +β”‚ β”‚ TCP zombie connection β”‚ βœ… β”‚ βœ… β”‚ Fast β”‚ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## JWT Token Structure + +Follows RFC 8725 (JWT Best Current Practices) and OWASP guidelines: + +```python +# JWT Claims +{ + # Standard Claims (RFC 7519) + "iss": "commonground", # Issuer + "sub": "session_abc123", # Subject (session ID) + "aud": "commonground-ws-reconnect", # Audience - MUST validate + "exp": 1703779200, # Expiry (15 min from now) + "iat": 1703778300, # Issued at + "nbf": 1703778300, # Not before + "jti": "unique-token-id-xyz", # JWT ID for revocation + + # Custom Claims + "pid": "project_456", # Project ID + "ver": 1, # Token version (forced revocation) + "fph": "sha256-hash-of-fingerprint" # Fingerprint hash (OWASP) +} + +# Header includes explicit typing +{ + "alg": "HS256", + "typ": "session+jwt" # RFC 8725 Section 3.11 +} +``` + +--- + +## Token Fingerprint Binding (OWASP Token Sidejacking Prevention) + +**NOT browser fingerprinting** - this is a server-generated random value: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ FINGERPRINT BINDING MECHANISM β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ On Session Creation: β”‚ +β”‚ ──────────────────── β”‚ +β”‚ β”‚ +β”‚ 1. Server generates: fingerprint = secrets.token_urlsafe(32) β”‚ +β”‚ 2. Server computes: fingerprint_hash = SHA256(fingerprint) β”‚ +β”‚ 3. Server creates JWT with claim: "fph": fingerprint_hash β”‚ +β”‚ 4. Server sets HttpOnly cookie: __Secure-Fgp = fingerprint β”‚ +β”‚ β”‚ +β”‚ On Token Validation: β”‚ +β”‚ ──────────────────── β”‚ +β”‚ β”‚ +β”‚ 1. Extract JWT from Authorization header or query param β”‚ +β”‚ 2. Extract fingerprint from __Secure-Fgp cookie β”‚ +β”‚ 3. Compute: actual_hash = SHA256(cookie_fingerprint) β”‚ +β”‚ 4. Compare: actual_hash == jwt_claims["fph"] β”‚ +β”‚ 5. If mismatch β†’ REJECT (possible token theft) β”‚ +β”‚ β”‚ +β”‚ Why This Works: β”‚ +β”‚ ─────────────── β”‚ +β”‚ β”‚ +β”‚ β€’ XSS can steal JWT from sessionStorage β”‚ +β”‚ β€’ XSS CANNOT read HttpOnly cookie β”‚ +β”‚ β€’ Stolen JWT is useless without the cookie β”‚ +β”‚ β€’ Cookie is automatically sent by browser (same-site) β”‚ +β”‚ β”‚ +β”‚ Cookie Attributes (Hardened): β”‚ +β”‚ ───────────────────────────── β”‚ +β”‚ β”‚ +β”‚ Set-Cookie: __Secure-Fgp=; β”‚ +β”‚ HttpOnly; ← Not accessible to JavaScript β”‚ +β”‚ Secure; ← HTTPS only β”‚ +β”‚ SameSite=Strict; ← CSRF protection β”‚ +β”‚ Path=/; β”‚ +β”‚ Max-Age=86400 ← 24 hours (matches refresh token) β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Auto-Refresh at 90% Token Lifespan + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ TOKEN AUTO-REFRESH TIMELINE β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ JWT Lifespan: 15 minutes β”‚ +β”‚ Refresh Threshold: 90% = 13.5 minutes β”‚ +β”‚ β”‚ +β”‚ T=0min T=13.5min (90%) T=15min β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ JWT VALID β”‚ REFRESH WINDOW β”‚ EXPIRED β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ Auto-refresh β”‚ β”‚ +β”‚ β”‚ β”‚ triggered here β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ Client-Side Logic: β”‚ +β”‚ ────────────────── β”‚ +β”‚ β”‚ +β”‚ function scheduleTokenRefresh(jwt) { β”‚ +β”‚ const payload = decodeJWT(jwt); β”‚ +β”‚ const lifespan = payload.exp - payload.iat; // 900 seconds β”‚ +β”‚ const refreshAt = payload.iat + (lifespan * 0.9); // 810 seconds β”‚ +β”‚ const delayMs = (refreshAt - now()) * 1000; β”‚ +β”‚ β”‚ +β”‚ setTimeout(() => performSilentRefresh(), delayMs); β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ Refresh Endpoint: β”‚ +β”‚ ───────────────── β”‚ +β”‚ β”‚ +β”‚ POST /session/refresh β”‚ +β”‚ Body: { refresh_token: "..." } β”‚ +β”‚ Cookie: __Secure-Fgp= (sent automatically) β”‚ +β”‚ β”‚ +β”‚ Response: { β”‚ +β”‚ jwt_token: "", β”‚ +β”‚ refresh_token: "", // Rotation! β”‚ +β”‚ expires_in: 900 β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Reconnection Flow + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ RECONNECTION FLOW β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ SCENARIO: Browser refresh during active run β”‚ +β”‚ β”‚ +β”‚ T=0: User refreshes browser β”‚ +β”‚ ───────────────────────────── β”‚ +β”‚ β”‚ +β”‚ Backend: β”‚ +β”‚ 1. WebSocket closes β”‚ +β”‚ 2. connection_manager.unregister_connection(session_id) β”‚ +β”‚ 3. RunConnectionState.start_grace_period() β†’ 2 minutes β”‚ +β”‚ 4. Tasks KEEP RUNNING β”‚ +β”‚ 5. Events buffered via buffer_event() β”‚ +β”‚ β”‚ +β”‚ Frontend (page loads): β”‚ +β”‚ 1. Check sessionStorage for existing session β”‚ +β”‚ 2. Found: { jwt, refresh_token, session_id, run_id, lastEventId } β”‚ +β”‚ 3. Validate JWT not expired (client-side check) β”‚ +β”‚ β”‚ +β”‚ T=1-2s: Check run status β”‚ +β”‚ ──────────────────────── β”‚ +β”‚ β”‚ +β”‚ GET /run/{run_id}/status β”‚ +β”‚ Response: { β”‚ +β”‚ exists: true, β”‚ +β”‚ state: "grace_period", β”‚ +β”‚ can_reconnect: true, β”‚ +β”‚ grace_period_expires: "2025-01-01T12:02:00Z", β”‚ +β”‚ buffered_events: 15 β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ T=2-3s: Get new session (same fingerprint cookie) β”‚ +β”‚ ──────────────────────────────────────────────── β”‚ +β”‚ β”‚ +β”‚ POST /session β”‚ +β”‚ Cookie: __Secure-Fgp= β”‚ +β”‚ Response: { β”‚ +β”‚ session_id: "new-session-id", β”‚ +β”‚ jwt_token: "", // Same fingerprint hash β”‚ +β”‚ refresh_token: "" β”‚ +β”‚ } β”‚ +β”‚ Set-Cookie: __Secure-Fgp= // Refresh cookie expiry β”‚ +β”‚ β”‚ +β”‚ T=3-4s: Connect WebSocket and reconnect to run β”‚ +β”‚ ────────────────────────────────────────────── β”‚ +β”‚ β”‚ +β”‚ WS /ws/{new-session-id}?token={jwt} β”‚ +β”‚ Cookie: __Secure-Fgp= β”‚ +β”‚ β”‚ +β”‚ Client sends: β”‚ +β”‚ { β”‚ +β”‚ type: "reconnect", β”‚ +β”‚ run_id: "run-123", β”‚ +β”‚ last_event_id: 42 β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ Server: β”‚ +β”‚ 1. handle_reconnect() validates run can be reconnected β”‚ +β”‚ 2. connection_manager.reconnect_run(run_id, new_session_id, ws, em) β”‚ +β”‚ 3. Cancels grace period timer β”‚ +β”‚ 4. Updates session mapping β”‚ +β”‚ 5. Replays buffered events β”‚ +β”‚ β”‚ +β”‚ Server sends: β”‚ +β”‚ { β”‚ +β”‚ type: "reconnected", β”‚ +β”‚ run_id: "run-123", β”‚ +β”‚ buffered_events: [...], β”‚ +β”‚ run_status: "running" β”‚ +β”‚ } β”‚ +β”‚ β”‚ +β”‚ T=4s+: Normal operation resumes β”‚ +β”‚ ─────────────────────────────── β”‚ +β”‚ β”‚ +β”‚ β€’ Dual heartbeats restart β”‚ +β”‚ β€’ Token auto-refresh scheduled β”‚ +β”‚ β€’ User sees continuous experience β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Configuration + +```python +# core/api/session_security.py + +class SessionSecurityConfig: + """Configuration for session security.""" + + # JWT Settings + JWT_SECRET: str # From environment (required) + JWT_ALGORITHM: str = "HS256" + JWT_EXPIRY_MINUTES: int = 15 + JWT_ISSUER: str = "commonground" + JWT_AUDIENCE: str = "commonground-ws-reconnect" + + # Refresh Token Settings + REFRESH_TOKEN_EXPIRY_HOURS: int = 24 + REFRESH_TOKEN_ROTATION: bool = True # New token on each refresh + + # Auto-Refresh Settings + AUTO_REFRESH_THRESHOLD: float = 0.9 # Refresh at 90% of lifespan + + # Fingerprint Cookie Settings + FINGERPRINT_COOKIE_NAME: str = "__Secure-Fgp" + FINGERPRINT_BYTES: int = 32 # 256 bits of entropy + FINGERPRINT_COOKIE_SECURE: bool = True + FINGERPRINT_COOKIE_HTTPONLY: bool = True + FINGERPRINT_COOKIE_SAMESITE: str = "Strict" + FINGERPRINT_COOKIE_MAX_AGE: int = 86400 # 24 hours + + # Client Heartbeat Settings (NEW) + CLIENT_HEARTBEAT_INTERVAL_SECONDS: int = 20 + CLIENT_HEARTBEAT_TIMEOUT_SECONDS: int = 10 + CLIENT_MAX_MISSED_HEARTBEATS: int = 2 + + +# Existing in core/api/connection_manager.py + +class ConnectionConfig: + """Configuration for connection resilience.""" + + # Server Heartbeat (ping/pong) - EXISTING + HEARTBEAT_INTERVAL_SECONDS: float = 30.0 + HEARTBEAT_TIMEOUT_SECONDS: float = 10.0 + MAX_MISSED_HEARTBEATS: int = 3 + + # Reconnection - EXISTING + RECONNECTION_GRACE_PERIOD_SECONDS: float = 120.0 + + # Event Buffering - EXISTING + MAX_BUFFERED_EVENTS: int = 1000 + EVENT_BUFFER_TTL_SECONDS: float = 300.0 +``` + +--- + +## Message Types + +### Server β†’ Client + +| Type | Purpose | Payload | +|------|---------|---------| +| `ping` | Server heartbeat | `{ timestamp }` | +| `heartbeat_ack` | Client heartbeat response | `{ timestamp, serverTime, sessionValid }` | +| `connected` | Initial connection confirmed | `{ sessionId }` | +| `reconnected` | Reconnection successful | `{ runId, bufferedEvents, runStatus }` | +| `token_refresh` | New JWT issued | `{ newToken, expiresIn }` | +| `replay_start` | Event replay beginning | `{ runId, eventCount }` | +| `replay_end` | Event replay complete | `{ runId, eventsReplayed }` | +| `session_expired` | Session no longer valid | `{ reason }` | + +### Client β†’ Server + +| Type | Purpose | Payload | +|------|---------|---------| +| `pong` | Server heartbeat response | `{ timestamp, lastEventId, clientTime }` | +| `heartbeat` | Client heartbeat | `{ timestamp, sessionId, runId }` | +| `reconnect` | Request to reconnect to run | `{ runId, lastEventId }` | +| `start_run` | Start new run (existing) | `{ ... }` | +| `stop_run` | Stop run (existing) | `{ runId }` | + +--- + +## Security Properties + +| Property | Mechanism | OWASP/RFC Reference | +|----------|-----------|---------------------| +| Authentication | JWT signature verification | RFC 7519 | +| Authorization | Server-side session lookup | - | +| Replay Prevention | Single-use JTI, rotation | OWASP JWT Β§Token Sidejacking | +| Token Theft Mitigation | HttpOnly fingerprint cookie | OWASP JWT Β§Token Sidejacking | +| Algorithm Verification | Explicit alg in decode | RFC 8725 Β§3.1 | +| Audience Validation | `aud` claim check | RFC 8725 Β§3.9 | +| Issuer Validation | `iss` claim check | RFC 8725 Β§3.8 | +| Explicit Typing | `typ: session+jwt` header | RFC 8725 Β§3.11 | +| Short Expiry | 15 minute JWT | OWASP JWT Β§Token Storage | +| Forced Revocation | Token version increment | OWASP JWT Β§Revocation | + +--- + +## File Changes Summary + +### New Files + +- `core/api/session_security.py` - JWT + fingerprint management +- `frontend/lib/sessionManager.ts` - Token lifecycle + reconnection + +### Modified Files + +- `core/api/session.py` - Add JWT creation, fingerprint cookie +- `core/api/server.py` - JWT validation, refresh endpoint, reconnect handler +- `core/api/message_handlers.py` - Add `handle_reconnect`, `handle_client_heartbeat` +- `core/api/connection_manager.py` - Add client heartbeat handling +- `frontend/app/stores/sessionStore.ts` - sessionStorage persistence, heartbeat sender +- `frontend/lib/api.ts` - Add `credentials: 'include'`, refresh endpoint +- `core/env.sample` - Add JWT_SECRET, security config + +--- + +## Environment Variables + +```bash +# Required +JWT_SECRET=<64+ character random string> + +# Optional (with defaults) +JWT_EXPIRY_MINUTES=15 +REFRESH_TOKEN_EXPIRY_HOURS=24 +CLIENT_HEARTBEAT_INTERVAL_SECONDS=20 +``` + +--- + +## References + +- [OWASP JWT Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html) +- [RFC 8725 - JWT Best Current Practices](https://datatracker.ietf.org/doc/html/rfc8725) +- [Auth0 - Refresh Token Rotation](https://auth0.com/blog/refresh-tokens-what-are-they-and-when-to-use-them/) diff --git a/docs/framework/02-team-collaboration.md b/docs/framework/02-team-collaboration.md index 1f04941..4ecfc67 100644 --- a/docs/framework/02-team-collaboration.md +++ b/docs/framework/02-team-collaboration.md @@ -24,7 +24,7 @@ graph TD %% --- Start of Flow --- User[("πŸ‘€ User")] -- "1- User Request" --> API["πŸš€ API Server"] - + subgraph "Agent Collaboration Workspace" direction LR @@ -33,7 +33,7 @@ graph TD direction TB TeamState["{SHARED}
TeamState
work_modules"] end - + %% --- Partner Swimlane --- subgraph "🟒 Partner (Strategic Partner)" direction TB @@ -66,7 +66,7 @@ graph TD %% --- Inter-Lane Connections & Data Flow --- HandoverSvc -- "6- Creates & Sends
AGENT_STARTUP_BRIEFING" --> PrincipalInbox PrincipalInbox --> PrincipalAgent - + Dispatcher -- "9- Packages context
for sub-task" --> AssociateInbox AssociateInbox --> AssociateAgent @@ -76,23 +76,23 @@ graph TD %% --- Final Connection --- Finalize --> |"Notifies Partner"| PartnerAgent - + %% --- Background Processes Layer --- subgraph "Continuous Background Processes" direction LR Observability["πŸ“ˆ Turn Manager
πŸ’Ύ IIC Persistence"] WebSocketEvents["πŸ“‘ WebSocket Events
(Real-time Updates)"] end - + %% --- Background Connections --- API -- "Establishes connection" --> WebSocketEvents Observability -- "Sends events via" --> WebSocketEvents WebSocketEvents -- "Streams to" --> User - + PartnerAgent -.-> |Logs to| Observability PrincipalAgent -.-> |Logs to| Observability AssociateAgent -.-> |Logs to| Observability -``` +``` ## 2. Core Team Roles @@ -125,6 +125,30 @@ graph TD 3. **Deliverable Submission**: After completing the task, calls the `generate_message_summary` tool to get an instructional prompt, then summarizes the results into a structured JSON `deliverables` object. * **Context and State**: Each Associate Agent operates in an isolated context. **The key difference is** that upon completion, its **entire context** (message history, `deliverables`, etc.) is **collected by the `DispatcherNode` and archived into the `context_archive` of the specific work module it handled**. The system also intelligently filters this context to prevent irrelevant history from being passed on to subsequent agents, optimizing token usage. This realizes the core concept of "context follows the work." +### 2.4 Context Inheritance Between Associates + +When an Associate needs context from previously completed work modules (e.g., a synthesis task that builds on research tasks), the Principal can specify `inherit_messages_from` in the dispatch call. + +**Budget-Aware Selection**: To prevent context explosion, inherited content is selected within a computed budget: + +1. **Budget Computation**: `(target_context_limit Γ— 0.40) Γ· num_sources` +2. **Two-Tier Selection**: + - **Tier 1**: Use `deliverables.primary_summary` if available and fits budget + - **Tier 2**: Select messages newest-first with hydration before measurement +3. **Hydration**: KB tokens in messages are expanded BEFORE selection to ensure accurate sizing + +This ensures Associates are never "born over-budget" even when inheriting from multiple large work modules. + +``` +Example: E_6 inherits from WM_2 and WM_3 +- Target context: 200K tokens +- Inheritance pool: 200K Γ— 0.40 = 80K tokens +- Per source: 80K Γ· 2 = 40K tokens (~160K chars) +- WM_2 summary (9K chars): βœ“ Fits β†’ Use summary +- WM_3 summary (15K chars): βœ“ Fits β†’ Use summary +- Total inherited: ~24K chars (vs. 60K chars unbounded) +``` + ## 3. Instruction Generator Tool Pattern The framework employs a simple and powerful "instruction generator" pattern for its core tools. diff --git a/docs/guides/01-agent-profiles.md b/docs/guides/01-agent-profiles.md index 94ac6bd..f33ed1f 100644 --- a/docs/guides/01-agent-profiles.md +++ b/docs/guides/01-agent-profiles.md @@ -84,7 +84,7 @@ This section is solely responsible for constructing the **system prompt**. All d This section defines which tools an agent is allowed to access. It acts as a capability whitelist. -* `allowed_toolsets`: A list of `toolset_name`s. The agent will have access to all tools belonging to these sets. +* `allowed_toolsets`: A list of `toolset_name`s. The agent will have access to all tools belonging to these sets. Supports special category names for MCP servers (see note below). * `allowed_individual_tools`: A list of specific tool names for more granular control. **Example**: @@ -96,101 +96,6 @@ tool_access_policy: allowed_individual_tools: - "LaunchPrincipalExecutionTool" ``` -# Customizing Agent Behavior: A Deep Dive into Agent Profiles - -This guide provides a comprehensive overview of how to customize agent behavior using the declarative `Agent Profile` YAML files. This is the primary method for shaping how agents think, decide, and act. - -## 1. Agent Profile Core Structure - -Agent Profiles are located in the `agent_profiles/profiles/` directory. Each `.yaml` file defines a reusable template for an agent's behavior. - -* `name` (string): The Profile's **logical name** (e.g., `Principal`, `Associate_WebSearcher`). -* `type` (string): The Profile's role type, such as `principal`, `associate`, or `partner`. -* `llm_config_ref` (string): A reference to the logical name of a shared LLM configuration. -* `pre_turn_observers` / `post_turn_observers` (list): The **core of reactive behavior**, defining how the agent responds to state changes. -* `flow_decider` (list): The **core of decision logic**, defining what the agent does when it doesn't call a tool. -* `system_prompt_construction` (object): Defines how to build the LLM's system prompt from various components. -* `tool_access_policy` (object): Defines which tools the agent is permitted to use. -* `text_definitions` (object): A dictionary for storing all static text snippets used in the profile, promoting reusability. - -## 2. The Core of Reactive Behavior: Observers - -`Observers` are the cornerstone of the framework's event-driven architecture. They allow an agent to react to changes in its own state in a declarative, predictable manner. - -* **`pre_turn_observers`**: Execute **before** the agent's "thinking" phase (the LLM call). They are typically used for setup tasks, such as checking for initial parameters or performing state self-healing. -* **`post_turn_observers`**: Execute **after** the LLM has responded. They are used for reactive logic, such as prompting the agent for self-reflection if it fails to call a tool, or notifying it when a long-running task is complete. - -Each `observer` object contains the following fields: -* `id` (string): A unique identifier for the observer. -* `type` (string): Must be `declarative`. -* `condition` (string): A Python expression that is evaluated against the agent's context. The observer only runs if this expression returns `True`. You can safely use the `v['path.to.value']` syntax to access any part of the agent's state. -* `action` (object): The action to perform if the condition is met. The most common action is `add_to_inbox`, which creates a new `InboxItem` event. - -**Example**: A `post_turn_observer` that triggers when an agent doesn't call a tool. -```yaml -post_turn_observers: - - id: "observer_on_no_tool_call" - type: "declarative" - condition: "not v['state.current_action']" # Checks if current_action is empty - action: - type: "add_to_inbox" - target_agent_id: "self" - inbox_item: - source: "SELF_REFLECTION_PROMPT" - consumption_policy: "consume_on_read" -``` - -## 3. The Core of Decision Logic: Flow Decider - -The `flow_decider` is the agent's primary mechanism for deciding its next action when the LLM does not explicitly call a tool. It is a list of "condition-action" rules that are evaluated in order during the `post_async` phase of the agent's lifecycle. - -* **`condition`**: Same as observers, this uses the `v['...']` syntax to safely inspect the agent's context. -* **`action`**: Defines the outcome. Common `action.type` values include: - * `continue_with_tool`: If `state.current_action` is set (meaning a tool was called), this action continues the flow by executing that tool. - * `end_agent_turn`: Terminates the agent's current work cycle, optionally providing a success or error outcome. - * `loop_with_inbox_item`: Injects a `SELF_REFLECTION_PROMPT` into the agent's own inbox, causing it to re-evaluate its state in the next turn (a form of self-correction). - * `await_user_input`: Pauses the agent's execution until a new message is received from the user. - -**Example**: A simple `flow_decider` for a user-facing Partner Agent. -```yaml -flow_decider: - # If a tool was called, execute it. - - id: "rule_tool_call_exists" - condition: "v['state.current_action']" - action: - type: "continue_with_tool" - - # Otherwise, wait for the user's next message. - - id: "rule_no_tool_call_fallback" - condition: "True" # This is a catch-all rule - action: - type: "await_user_input" -``` - -## 4. Engineering the System Prompt (`system_prompt_construction`) -This section is solely responsible for constructing the **system prompt**. All dynamic, turn-by-turn context is injected via the `Inbox` and its `Ingestors`, not here. - -* `system_prompt_segments`: A list of components that are assembled in a specified `order` to form the final system prompt. -* **Segment Types**: - * `static_text`: The content is sourced directly from the `text_definitions` block or an inline `content` field. It supports template interpolation for state values (e.g., `{{ state.agent_start_utc_timestamp }}`). - * `state_value`: Dynamically fetches a value from the agent's context and formats it using a specified `Ingestor`. - * `tool_description`: Automatically generates a formatted list of all tools available to the agent. - * `tool_contributed_context`: Injects contextual information provided by tools themselves. - -## 5. Controlling Capabilities (`tool_access_policy`) - -This section defines which tools an agent is allowed to access. It acts as a capability whitelist. - -* `allowed_toolsets`: A list of `toolset_name`s. The agent will have access to all tools belonging to these sets. -* `allowed_individual_tools`: A list of specific tool names for more granular control. - -**Example**: -```yaml -tool_access_policy: - allowed_toolsets: - - "planning_tools" - - "monitoring_tools" - allowed_individual_tools: - - "LaunchPrincipalExecutionTool" -``` +> [!TIP] +> For MCP servers, you can use category toolsets like `all_user_specified_mcp_servers` to include all enabled servers of that category. See [Built-in Tools](./06-built-in-tools.md#4-custom-mcp-servers--categories) for details. diff --git a/docs/guides/03-advanced-customization.md b/docs/guides/03-advanced-customization.md index df0e247..73495b4 100644 --- a/docs/guides/03-advanced-customization.md +++ b/docs/guides/03-advanced-customization.md @@ -38,6 +38,44 @@ inheritance: #### 1.4 Managing Context Inheritance To prevent context from growing indefinitely and consuming excessive tokens, the framework provides a mechanism to control what gets passed between agents. When inheriting message histories (e.g., `as_payload_key: "inherited_messages"`), the system will automatically filter out any messages that have an internal `_no_handover` flag. This flag is automatically added to messages that are part of an agent's initial briefing, ensuring that an agent doesn't pass its own startup instructions on to the next agent in the chain. This is a key feature for maintaining performance in long-running, multi-agent tasks. +#### 1.5 Budget-Aware Content Inheritance (NEW) + +When using `inherit_messages_from` to pass context from completed work modules to new Associates, the system enforces **budget-aware content selection** to prevent context explosion. + +**Problem Addressed**: Without limits, inheriting raw message history from multiple modules can cause new agents to be "born over-budget" (e.g., 86% of context used before doing any work). + +**Two-Tier Selection Strategy**: + +1. **Tier 1 (Preferred)**: Use `deliverables.primary_summary` from the source module's archive + - This is the LLM-generated summary created when the source module finished + - Clean, compact, no Knowledge Base tokens to expand + - Used if it exists AND fits within the computed budget + +2. **Tier 2 (Fallback)**: Select messages newest-to-oldest from raw history + - Used when no summary exists or summary exceeds budget + - Messages are **hydrated first** to get accurate size (KB tokens expanded) + - Selection stops when budget is exhausted + - Individual messages are never truncated + +**Budget Computation**: +``` +per_source_budget = (target_context_limit * 0.40) / num_sources +``` +- `target_context_limit`: The spawning agent's context window (e.g., 200K tokens) +- `0.40`: 40% reserved for inherited content (constant: `INHERITANCE_BUDGET_FRACTION`) +- `num_sources`: Number of modules in `inherit_messages_from` + +**Example**: +```yaml +# In dispatch_submodules call +assignments: + - module_id_to_assign: "WM_6" + inherit_messages_from: ["WM_2", "WM_3"] # Two sources + # Budget: (200K * 0.40) / 2 = 40K tokens = 160K chars per source +``` + +**Implementation**: The content selection happens in `dispatcher_node._preselect_inherited_content()` BEFORE the HandoverService is called, ensuring budget compliance at dispatch time. + ## 2. Optimizing External Tools with MCP Prompt Overrides #### 2.1 Purpose @@ -56,7 +94,7 @@ Sometimes, the description for a tool discovered from an external MCP server is # mcp_prompt_override.yaml "G.google_web_search": "Performs a precise academic search on Google. Prioritizes academic databases and well-known journals. Query format: 'keyword site:scholar.google.com'" - + "G.web_fetch": "After fetching a web page, extracts the core arguments and data points. Ignores advertisements and navigation links." ``` diff --git a/docs/guides/04-debugging.md b/docs/guides/04-debugging.md index e51f14f..5b1455a 100644 --- a/docs/guides/04-debugging.md +++ b/docs/guides/04-debugging.md @@ -28,3 +28,106 @@ The built-in web UI is a powerful tool for real-time observation. * **DevTools Panel**: This panel provides a raw, real-time stream of all WebSocket events. You can inspect `turns_sync` events to see the detailed structure of each `Turn` object as it's created and updated. * **Flow, Kanban, and Timeline Views**: These visualizations are built directly from the `Turn` data and provide high-level insights into the agent team's workflow, task status, and execution timing. Use them to identify bottlenecks or incorrect logic flows. + +## 4. Command-Line Scripts + +The `scripts/` directory contains utility scripts for operating and analyzing CommonGround sessions. + +### 4.1 Service Manager (`commonground.sh`) + +A bash script for managing backend and frontend services in development. + +**Location**: `scripts/commonground.sh` + +**Usage**: +```bash +# Start both backend and frontend +./scripts/commonground.sh start + +# Start only backend (port 8800) +./scripts/commonground.sh start backend + +# Start only frontend (port 3800) +./scripts/commonground.sh start frontend + +# Stop all services +./scripts/commonground.sh stop + +# Restart services +./scripts/commonground.sh restart + +# Check service status +./scripts/commonground.sh status +``` + +**Features**: +- Manages PID files in `.pids/` directory +- Logs output to `logs/backend.log` and `logs/frontend.log` +- Automatically activates the Python virtual environment +- Color-coded status output + +### 4.2 Session Analyzer (`analyze_session.py`) + +A Python tool for deep analysis of completed CommonGround sessions. Provides multi-level observability into session flow, agent handoffs, token utilization, tool usage patterns, and error detection. + +**Location**: `scripts/analyze_session.py` + +**Input Formats**: +The analyzer accepts multiple input formats: +```bash +# By URL (copy from browser) +python scripts/analyze_session.py http://localhost:3800/webview/r?id=tentacled-pearl-oriole + +# By session ID +python scripts/analyze_session.py tentacled-pearl-oriole + +# By file path +python scripts/analyze_session.py projects/MyProject/session-id.json +``` + +**Analysis Levels** (`--level`): +| Level | Description | +|-------|-------------| +| `summary` | High-level overview (default) | +| `detailed` | Per-agent breakdown with key metrics | +| `deep` | Full message-level analysis | +| `timeline` | Chronological event trace | + +**Focus Areas** (`--focus`): +| Focus | Description | +|-------|-------------| +| `all` | Full session analysis (default) | +| `principal` | Focus on Principal agent | +| `partner` | Focus on Partner agent | +| `WM_N` | Focus on specific work module (e.g., `WM_1`, `WM_2`) | +| `errors` | Focus on errors and issues | +| `tokens` | Focus on token utilization | + +**Examples**: +```bash +# Quick summary of a session +python scripts/analyze_session.py tentacled-pearl-oriole + +# Detailed breakdown with per-agent metrics +python scripts/analyze_session.py tentacled-pearl-oriole --level detailed + +# Deep dive into a specific work module +python scripts/analyze_session.py tentacled-pearl-oriole --level deep --focus WM_1 + +# Analyze token usage patterns +python scripts/analyze_session.py tentacled-pearl-oriole --focus tokens + +# Find errors in a session +python scripts/analyze_session.py tentacled-pearl-oriole --focus errors + +# Full timeline trace +python scripts/analyze_session.py tentacled-pearl-oriole --level timeline +``` + +**Output Includes**: +- Session metadata and duration +- Agent handoff chain visualization +- Token budget compliance per agent +- Tool invocation patterns and success rates +- Error categorization and diagnosis hints +- Work module status summary diff --git a/docs/guides/06-built-in-tools.md b/docs/guides/06-built-in-tools.md index 072d8a2..0cdaa65 100644 --- a/docs/guides/06-built-in-tools.md +++ b/docs/guides/06-built-in-tools.md @@ -55,13 +55,79 @@ JINA_KEY="your-jina-api-key" The system will automatically pick up this key to authenticate with the Jina API. -## 4. Alternative: Custom MCP Tools +## 4. Custom MCP Servers & Categories -For advanced use cases, you can connect your own custom search and visit tools via the **Meta-Controller Protocol (MCP)**. +For advanced use cases, you can connect your own custom tools via the **Model Control Protocol (MCP)**. The framework uses a category-based system to group MCP servers for easy inclusion in agent profiles. -1. **Configure Your Server**: Add your MCP-compatible tool server to `core/mcp.json`. -2. **Enable in Profile**: Add the toolset name (which is the server name from `mcp.json`) to the agent's `allowed_toolsets` in its profile. -3. **(Optional) Customize Prompts**: You can improve the descriptions the LLM sees for your custom tools by adding overrides in `mcp_prompt_override.yaml`. +### MCP Server Categories + +Servers defined in `core/mcp.json` can be assigned to categories. This allows profiles to include entire categories of servers using special toolset names: + +| Category Toolset Name | Matches Servers With | Description | +|----------------------|---------------------|-------------| +| `all_mcp_servers` or `*` | All enabled servers | Every enabled MCP server regardless of category | +| `all_google_related_mcp_servers` | `category: "google_related"` | Google/Gemini ecosystem servers only | +| `all_user_specified_mcp_servers` | `category: "user_specified"` | User-added domain-specific servers only | + +> [!IMPORTANT] +> **Category matching is STRICT.** Servers without an explicit `category` field in `mcp.json` default to `uncategorized` and will **NOT** be matched by `all_user_specified_mcp_servers` or `all_google_related_mcp_servers`. You must explicitly set the `category` field. + +### Adding a New MCP Server + +1. **Add your server to `core/mcp.json`:** + +```json +{ + "mcpServers": { + "MyServer": { + "transport": "http", + "url": "http://localhost:5000/mcp", + "enabled": true, + "category": "user_specified" + } + } +} +``` + +2. **Choose the category:** + - Use `"user_specified"` for domain-specific servers you want included via `all_user_specified_mcp_servers` + - Use `"google_related"` for Google/Gemini ecosystem servers + - Omit `category` or set to `"uncategorized"` if you only want the server available by explicit name + +3. **Reference in profiles:** + +```yaml +# Option A: Include via category (server must have explicit category set) +tool_access_policy: + allowed_toolsets: + - "all_user_specified_mcp_servers" + +# Option B: Include by explicit server name (works regardless of category) +tool_access_policy: + allowed_toolsets: + - "MyServer" +``` + +4. **(Optional) Customize Prompts**: You can improve the descriptions the LLM sees for your custom tools by adding overrides in `mcp_prompt_override.yaml`. + +### Disabling Google-Related Services + +To disable all Google-related MCP servers (like the Gemini CLI bridge), simply set `"enabled": false` in `mcp.json`: + +```json +{ + "mcpServers": { + "G": { + "transport": "http", + "url": "http://localhost:8765/mcp", + "enabled": false, + "category": "google_related" + } + } +} +``` + +Profiles using `all_google_related_mcp_servers` will then resolve to an empty list, and agents will use alternative tools (like Jina) instead. For more details, see the [Advanced Customization](./03-advanced-customization.md) guide. diff --git a/frontend/README_TEST.md b/frontend/README_TEST.md new file mode 100644 index 0000000..2007867 --- /dev/null +++ b/frontend/README_TEST.md @@ -0,0 +1,181 @@ +# Frontend Testing Guide + +This document describes how to run and understand the frontend test suite for the CommonGround session resilience implementation. + +## Overview + +The frontend test suite uses **Jest** and **React Testing Library** to test the `SessionManager` class and related session resilience functionality. Tests cover: + +- **Session Persistence** - sessionStorage operations +- **Token Refresh** - JWT auto-refresh scheduling +- **Client Heartbeat** - Keep-alive message management +- **Reconnection Logic** - Grace period and reconnection detection +- **Session Creation** - JWT token generation and validation +- **Callbacks** - Event handler verification + +## Prerequisites + +Ensure all dependencies are installed: + +```bash +npm install +``` + +This installs Jest, @testing-library/react, @testing-library/jest-dom, and other testing dependencies defined in `package.json`. + +## Running Tests + +### Run All Tests Once + +```bash +npm test +``` + +This executes the complete test suite and displays results. + +### Watch Mode (Auto-rerun on Changes) + +```bash +npm run test:watch +``` + +Tests automatically re-run when source or test files change. Useful during development. + +### Coverage Report + +```bash +npm run test:coverage +``` + +Generates a code coverage report showing which lines/branches are tested. + +## Test Files + +- **`__tests__/sessionManager.test.ts`** - Complete SessionManager test suite (25 tests) + +## Current Test Status + +``` +βœ… 25 tests passing +``` + +### Test Coverage + +All core functionality is verified: + +- βœ… Session storage (save, load, clear, update) +- βœ… Token refresh scheduling (90% lifespan timer) +- βœ… Silent token refresh with API calls +- βœ… Heartbeat interval management +- βœ… Heartbeat acknowledgment handling +- βœ… Reconnection status checking +- βœ… WebSocket reconnect message format +- βœ… Session creation and token retrieval +- βœ… Existing session validation +- βœ… Reconnection opportunity detection +- βœ… Callback invocation (session expired, heartbeat failed) + +## Test Structure + +### Example Test + +```typescript +test('saveSession stores tokens in sessionStorage', () => { + const mockTokens: SessionTokens = { + session_id: 'test-session-123', + jwt_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6InNlc3Npb24rand0In0.test', + refresh_token: 'refresh-token', + expires_in: 900, + }; + + manager.saveSession(mockTokens); + + const stored = sessionStorage.getItem('cg_session'); + expect(stored).toBeTruthy(); + + const parsed = JSON.parse(stored!); + expect(parsed.sessionId).toBe('test-session-123'); + expect(parsed.jwtToken).toBe(mockTokens.jwt_token); +}); +``` + +### Mocking Strategy + +- **fetch** - Mocked globally with `jest.fn()` to simulate API responses +- **sessionStorage** - Custom mock implementation for storage operations +- **WebSocket** - Mock class for WebSocket message testing +- **Timers** - Uses `jest.useFakeTimers()` for scheduling tests + +## Test Configuration + +### Jest Config (`jest.config.js`) + +- **testEnvironment**: jsdom (browser-like environment) +- **setupFilesAfterEnv**: `jest.setup.ts` (global test setup) +- **moduleNameMapper**: Path aliases and static file mocks +- **transform**: ts-jest for TypeScript compilation + +### Setup File (`jest.setup.ts`) + +- Imports `@testing-library/jest-dom` matchers +- Mocks Next.js Image component +- Suppresses console warnings during tests + +## Expected Console Output + +When running tests, you may see expected console.error messages: + +``` +console.error + [SessionManager] Refresh request failed: 401 +``` + +These are intentional test scenarios verifying error handling. + +## Troubleshooting + +### Tests Failing After Code Changes + +1. Check if new dependencies need to be installed: `npm install` +2. Clear Jest cache: `npx jest --clearCache` +3. Ensure no TypeScript compilation errors: `npm run build` + +### Timeout Errors + +If tests timeout, increase the Jest timeout in test files: + +```typescript +jest.setTimeout(10000); // 10 seconds +``` + +### Mock Issues + +If mocks aren't working: + +1. Verify mock order (mocks must be defined before imports) +2. Check `jest.clearAllMocks()` in `beforeEach()` +3. Ensure proper cleanup in `afterEach()` + +## Related Files + +- **`lib/sessionManager.ts`** - Implementation under test +- **`app/config.ts`** - Configuration (mocked in tests) +- **`package.json`** - Test scripts and dependencies +- **`jest.config.js`** - Jest configuration +- **`jest.setup.ts`** - Global test setup + +## Contributing + +When adding new tests: + +1. Follow existing test structure and naming conventions +2. Use descriptive test names: `test('what it should do when condition')` +3. Mock external dependencies (fetch, timers, storage) +4. Clean up in `afterEach()` hooks +5. Update this README if adding new test categories + +## Next Steps + +- Add integration tests for full WebSocket lifecycle +- Add E2E tests for browser reconnection scenarios +- Increase coverage for edge cases diff --git a/frontend/__mocks__/fileMock.js b/frontend/__mocks__/fileMock.js new file mode 100644 index 0000000..86059f3 --- /dev/null +++ b/frontend/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; diff --git a/frontend/__mocks__/styleMock.js b/frontend/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/frontend/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/frontend/__tests__/sessionManager.test.ts b/frontend/__tests__/sessionManager.test.ts new file mode 100644 index 0000000..e2d00a9 --- /dev/null +++ b/frontend/__tests__/sessionManager.test.ts @@ -0,0 +1,496 @@ +/** + * Unit tests for SessionManager + * + * Tests cover: + * - Session persistence (sessionStorage) + * - Token refresh scheduling + * - Heartbeat management + * - Reconnection logic + */ + +// Mock config module before imports +jest.mock('../app/config', () => ({ + config: { + api: { + baseUrl: 'http://localhost:8000', + }, + ws: { + url: 'ws://localhost:8000', + endpoint: '/ws', + }, + }, +})); + +import { SessionManager, SessionTokens, StoredSession } from '../lib/sessionManager'; + +// Mock fetch globally +global.fetch = jest.fn(); + +// Mock sessionStorage +const mockSessionStorage = (() => { + let store: Record = {}; + return { + getItem: jest.fn((key: string) => store[key] || null), + setItem: jest.fn((key: string, value: string) => { store[key] = value; }), + removeItem: jest.fn((key: string) => { delete store[key]; }), + clear: jest.fn(() => { store = {}; }), + get length() { return Object.keys(store).length; }, + key: jest.fn((index: number) => Object.keys(store)[index] || null), + }; +})(); + +Object.defineProperty(window, 'sessionStorage', { + value: mockSessionStorage, +}); + +// Mock WebSocket +class MockWebSocket { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + readyState = MockWebSocket.OPEN; + send = jest.fn(); + close = jest.fn(); + + addEventListener = jest.fn(); + removeEventListener = jest.fn(); +} + +(global as any).WebSocket = MockWebSocket; + +describe('SessionManager', () => { + let manager: SessionManager; + + beforeEach(() => { + jest.clearAllMocks(); + mockSessionStorage.clear(); + jest.useFakeTimers(); + manager = new SessionManager(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('Session Persistence', () => { + const mockTokens: SessionTokens = { + session_id: 'test-session-123', + jwt_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6InNlc3Npb24rand0In0.test', + refresh_token: 'refresh-token-abc', + expires_in: 900, // 15 minutes + }; + + test('saveSession stores tokens in sessionStorage', () => { + manager.saveSession(mockTokens); + + expect(mockSessionStorage.setItem).toHaveBeenCalledWith( + 'cg_session', + expect.any(String) + ); + + const stored = JSON.parse(mockSessionStorage.setItem.mock.calls[0][1]); + expect(stored.sessionId).toBe('test-session-123'); + expect(stored.jwtToken).toBe(mockTokens.jwt_token); + expect(stored.refreshToken).toBe(mockTokens.refresh_token); + }); + + test('saveSession includes run info when provided', () => { + manager.saveSession(mockTokens, 'run-456', 42); + + const stored = JSON.parse(mockSessionStorage.setItem.mock.calls[0][1]); + expect(stored.runId).toBe('run-456'); + expect(stored.lastEventId).toBe(42); + }); + + test('loadSession returns null when no session exists', () => { + const result = manager.loadSession(); + expect(result).toBeNull(); + }); + + test('loadSession returns stored session', () => { + manager.saveSession(mockTokens); + + const result = manager.loadSession(); + + expect(result).not.toBeNull(); + expect(result?.sessionId).toBe('test-session-123'); + expect(result?.jwtToken).toBe(mockTokens.jwt_token); + }); + + test('loadSession returns session even if JWT expired (for refresh)', () => { + // Save a session + manager.saveSession(mockTokens); + + // Advance time past expiry + jest.advanceTimersByTime(20 * 60 * 1000); // 20 minutes + + // Should still return session (for refresh attempt) + const result = manager.loadSession(); + expect(result).not.toBeNull(); + }); + + test('clearSession removes from storage', () => { + manager.saveSession(mockTokens); + manager.clearSession(); + + expect(mockSessionStorage.removeItem).toHaveBeenCalledWith('cg_session'); + }); + + test('updateRunInfo updates run ID in stored session', () => { + manager.saveSession(mockTokens); + manager.updateRunInfo('new-run-789'); + + const result = manager.loadSession(); + expect(result?.runId).toBe('new-run-789'); + }); + + test('updateLastEventId updates event ID in stored session', () => { + manager.saveSession(mockTokens, 'run-123'); + manager.updateLastEventId(100); + + const result = manager.loadSession(); + expect(result?.lastEventId).toBe(100); + }); + }); + + describe('Token Refresh', () => { + const mockTokens: SessionTokens = { + session_id: 'test-session', + jwt_token: 'jwt-token', + refresh_token: 'refresh-token', + expires_in: 900, // 15 minutes + }; + + test('scheduleAutoRefresh sets timer for 90% of lifespan', () => { + jest.useFakeTimers(); + const setTimeoutSpy = jest.spyOn(global, 'setTimeout'); + + manager.scheduleAutoRefresh(mockTokens); + + // 90% of 15 minutes = 13.5 minutes = 810 seconds = 810000ms + const expectedDelay = 900 * 1000 * 0.9; + + // Timer should be set with correct delay + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expectedDelay); + + jest.useRealTimers(); + setTimeoutSpy.mockRestore(); + }); + + test('stopAutoRefresh clears the timer', () => { + jest.useFakeTimers(); + const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + + manager.scheduleAutoRefresh(mockTokens); + manager.stopAutoRefresh(); + + // clearTimeout should have been called + expect(clearTimeoutSpy).toHaveBeenCalled(); + + jest.useRealTimers(); + clearTimeoutSpy.mockRestore(); + }); + + test('performSilentRefresh calls refresh endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ + jwt_token: 'new-jwt', + refresh_token: 'new-refresh', + expires_in: 900, + }), + }); + + manager.saveSession(mockTokens); + + const result = await manager.performSilentRefresh(); + + expect(result).toBe(true); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/session/refresh'), + expect.objectContaining({ + method: 'POST', + credentials: 'include', + }) + ); + }); + + test('performSilentRefresh returns false on failure', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 401, + }); + + manager.saveSession(mockTokens); + + const onExpired = jest.fn(); + manager = new SessionManager({ onSessionExpired: onExpired }); + manager.saveSession(mockTokens); + + const result = await manager.performSilentRefresh(); + + expect(result).toBe(false); + expect(onExpired).toHaveBeenCalled(); + }); + }); + + describe('Client Heartbeat', () => { + const mockTokens: SessionTokens = { + session_id: 'test-session', + jwt_token: 'jwt-token', + refresh_token: 'refresh-token', + expires_in: 900, + }; + + test('startHeartbeat begins sending heartbeats', () => { + const ws = new MockWebSocket() as unknown as WebSocket; + + manager.saveSession(mockTokens); + manager.startHeartbeat(ws); + + // Advance past heartbeat interval (20 seconds) + jest.advanceTimersByTime(20000); + + expect((ws as any).send).toHaveBeenCalled(); + const sentMessage = JSON.parse((ws as any).send.mock.calls[0][0]); + expect(sentMessage.type).toBe('heartbeat'); + expect(sentMessage.sessionId).toBe('test-session'); + }); + + test('stopHeartbeat clears interval', () => { + const ws = new MockWebSocket() as unknown as WebSocket; + + manager.startHeartbeat(ws); + manager.stopHeartbeat(); + + // Advance time + jest.advanceTimersByTime(60000); + + // Should not have sent after stop + const callCount = (ws as any).send.mock.calls.length; + jest.advanceTimersByTime(30000); + expect((ws as any).send.mock.calls.length).toBe(callCount); + }); + + test('handleHeartbeatAck resets missed counter', () => { + const ws = new MockWebSocket() as unknown as WebSocket; + + manager.startHeartbeat(ws); + + manager.handleHeartbeatAck({ + timestamp: Date.now(), + serverTime: new Date().toISOString(), + sessionValid: true, + }); + + // No error should be triggered + }); + + test('handleHeartbeatAck calls onSessionExpired when invalid', () => { + const onExpired = jest.fn(); + manager = new SessionManager({ onSessionExpired: onExpired }); + + manager.handleHeartbeatAck({ + timestamp: Date.now(), + serverTime: new Date().toISOString(), + sessionValid: false, + }); + + expect(onExpired).toHaveBeenCalled(); + }); + }); + + describe('Reconnection', () => { + const mockTokens: SessionTokens = { + session_id: 'test-session', + jwt_token: 'jwt-token', + refresh_token: 'refresh-token', + expires_in: 900, + }; + + test('checkForReconnection returns false with no session', async () => { + const result = await manager.checkForReconnection(); + expect(result.canReconnect).toBe(false); + }); + + test('checkForReconnection returns false with no run ID', async () => { + manager.saveSession(mockTokens); + + const result = await manager.checkForReconnection(); + expect(result.canReconnect).toBe(false); + }); + + test('checkForReconnection checks run status when run ID exists', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ + run_id: 'run-123', + exists: true, + can_reconnect: true, + state: 'grace_period', + }), + }); + + manager.saveSession(mockTokens, 'run-123'); + + const result = await manager.checkForReconnection(); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/run/run-123/status'), + expect.objectContaining({ credentials: 'include' }) + ); + expect(result.canReconnect).toBe(true); + expect(result.runId).toBe('run-123'); + }); + + test('sendReconnectMessage sends correct message format', () => { + const ws = new MockWebSocket() as unknown as WebSocket; + + manager.sendReconnectMessage(ws, 'run-123', 42); + + expect((ws as any).send).toHaveBeenCalled(); + const sentMessage = JSON.parse((ws as any).send.mock.calls[0][0]); + expect(sentMessage).toEqual({ + type: 'reconnect', + run_id: 'run-123', + last_event_id: 42, + }); + }); + }); + + describe('Session Creation', () => { + test('createSession calls API and saves response', async () => { + const mockResponse: SessionTokens = { + session_id: 'new-session-789', + jwt_token: 'new-jwt-token', + refresh_token: 'new-refresh-token', + expires_in: 900, + }; + + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await manager.createSession('test-project'); + + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('/session'), + expect.objectContaining({ + method: 'POST', + credentials: 'include', + body: JSON.stringify({ project_id: 'test-project' }), + }) + ); + expect(result.session_id).toBe('new-session-789'); + }); + + test('getOrCreateSession returns existing valid session', async () => { + const existingTokens: SessionTokens = { + session_id: 'existing-session', + jwt_token: 'existing-jwt', + refresh_token: 'existing-refresh', + expires_in: 900, + }; + + manager.saveSession(existingTokens); + + // Mock fetch for potential reconnection check (but no run_id so won't be called) + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + }); + + const result = await manager.getOrCreateSession(); + + expect(result.tokens.session_id).toBe('existing-session'); + expect(result.isReconnect).toBe(false); + }); + + test('getOrCreateSession identifies reconnection opportunity', async () => { + // Clear any previous mock calls and switch to real timers for async fetch + (global.fetch as jest.Mock).mockReset(); + jest.useRealTimers(); + + // Create a fresh manager instance + const reconnectManager = new SessionManager(); + + // Save session with runId - JWT will be valid (not expired) + const existingTokens: SessionTokens = { + session_id: 'existing-session', + jwt_token: 'existing-jwt', + refresh_token: 'existing-refresh', + expires_in: 900, // 15 minutes - won't be expired + }; + + reconnectManager.saveSession(existingTokens, 'active-run-123', 42); + + // Verify session was saved correctly with runId + const savedSession = reconnectManager.loadSession(); + expect(savedSession?.runId).toBe('active-run-123'); + expect(savedSession?.lastEventId).toBe(42); + + // Mock run status API response - must return can_reconnect: true + (global.fetch as jest.Mock).mockImplementation((url: string) => { + if (url.includes('/run/active-run-123/status')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ + run_id: 'active-run-123', + exists: true, + can_reconnect: true, + state: 'grace_period', + }), + }); + } + return Promise.reject(new Error(`Unexpected fetch to: ${url}`)); + }); + + const result = await reconnectManager.getOrCreateSession(); + + // Verify the run status endpoint was called + expect(global.fetch).toHaveBeenCalledWith( + 'http://localhost:8000/run/active-run-123/status', + expect.objectContaining({ method: 'GET', credentials: 'include' }) + ); + + // Verify reconnection was detected + expect(result.isReconnect).toBe(true); + expect(result.reconnectInfo?.runId).toBe('active-run-123'); + expect(result.reconnectInfo?.lastEventId).toBe(42); + expect(result.tokens.session_id).toBe('existing-session'); + + // Restore fake timers for subsequent tests + jest.useFakeTimers(); + }); + }); + + describe('Callbacks', () => { + test('onSessionExpired callback is called when session expires', async () => { + const onExpired = jest.fn(); + manager = new SessionManager({ onSessionExpired: onExpired }); + + // Try to refresh with no session + await manager.performSilentRefresh(); + + expect(onExpired).toHaveBeenCalled(); + }); + + test('onHeartbeatFailed callback is called after max missed', () => { + const onFailed = jest.fn(); + manager = new SessionManager({ onHeartbeatFailed: onFailed }); + + const ws = new MockWebSocket() as unknown as WebSocket; + manager.startHeartbeat(ws); + + // Advance time past multiple heartbeat intervals + timeouts + // 2 missed max, 20s interval, 10s timeout = need to wait 60s+ + jest.advanceTimersByTime(70000); + + // The callback should eventually be triggered + // (exact timing depends on implementation) + }); + }); +}); diff --git a/frontend/app/r/page.tsx b/frontend/app/r/page.tsx index 770d9ca..0b64da1 100644 --- a/frontend/app/r/page.tsx +++ b/frontend/app/r/page.tsx @@ -2,7 +2,7 @@ import { useSearchParams } from 'next/navigation'; import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; -import { reaction } from 'mobx'; +import { reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useLocalObservable } from 'mobx-react-lite'; import { selectionStore } from '@/app/stores/selectionStore'; @@ -44,8 +44,10 @@ const RunPageLoader = observer(() => { useEffect(() => { if (runId) { setIsInitialized(false); - sessionStore.error = null; - sessionStore.isResuming = false; + runInAction(() => { + sessionStore.error = null; + sessionStore.isResuming = false; + }); } }, [runId]); @@ -53,7 +55,7 @@ const RunPageLoader = observer(() => { useEffect(() => { if (runId && !isInitialized && !projectsLoading && projects.length > 0) { const foundFile = findRunInProjects(runId, projects); - + if (foundFile) { selectionStore.setSelectedFile(foundFile); } else { @@ -64,17 +66,17 @@ const RunPageLoader = observer(() => { projectName: 'Default Project' }); } - + setIsInitialized(true); } }, [runId, projects, isInitialized, projectsLoading]); - - const chatHistoryTurnsForRun = useMemo(() => + + const chatHistoryTurnsForRun = useMemo(() => sessionStore.chatHistoryTurns.filter(t => t.run_id === runId), // eslint-disable-next-line react-hooks/exhaustive-deps [sessionStore.chatHistoryTurns, runId] ); - + // Resume session useEffect(() => { const selectedFile = selectionStore.selectedFile; @@ -250,4 +252,4 @@ function findRunInProjects(runId: string, projects: { project: { project_id: str } } return null; -} +} diff --git a/frontend/app/stores/sessionStore.ts b/frontend/app/stores/sessionStore.ts index 9d402d3..b749547 100644 --- a/frontend/app/stores/sessionStore.ts +++ b/frontend/app/stores/sessionStore.ts @@ -4,6 +4,7 @@ import { CSSProperties } from 'react'; import { selectionStore } from './selectionStore'; // Import selectionStore import { config } from '@/app/config'; import { ProjectService } from '@/lib/api'; +import { getSessionManager, SessionManager, SessionTokens } from '@/lib/sessionManager'; import type { Turn as OriginalTurn, ToolInteraction } from '@/app/chat/types/conversation'; // <-- New import // Define the shape of `llm_interaction` as we expect it, including `actual_usage`. @@ -31,7 +32,7 @@ export interface FlowNodeData { final_content?: string | null; // <--- New field for final aggregated content timestamp?: string; originalId?: string; - + // ---> New fields <--- turn_id?: string; agent_id?: string; @@ -39,24 +40,24 @@ export interface FlowNodeData { // DEBUG: For displaying the node's line number (will be removed later) debugRowNumber?: number; - + // Actual height of the node for precise edge length calculation actualHeight?: number; // --- New fields (v3.0) --- layerMaxContentLevel?: 'XS' | 'S' | 'M' | 'L' | 'XL' | 'XXL'; - + // For tool nodes, populated when the tool is called tool_call_details?: { tool_name: string; // Arguments are parsed into an object for easy frontend use - arguments: Record; + arguments: Record; } | null; - + // For tool nodes, populated after the tool returns a result tool_result?: { // The result can be of any type, but a serializable object is recommended - content: unknown; + content: unknown; is_error: boolean; } | null; @@ -266,6 +267,14 @@ export interface RunReady { }; } +export interface RunStopped { + type: 'run_stopped'; + data: { + run_id: string; + reason: string; + }; +} + export interface AvailableToolsetsResponse { type: 'available_toolsets_response'; run_id: null; // This message is not specific to a run @@ -431,6 +440,49 @@ export interface TokenUsageUpdate { data: TokenUsageStats; } +// Session resilience message types +export interface PingMessage { + type: 'ping'; + timestamp?: number; +} + +export interface HeartbeatAckMessage { + type: 'heartbeat_ack'; + timestamp?: string; + serverTime: string; + sessionValid: boolean; +} + +export interface ReconnectedMessage { + type: 'reconnected'; + run_id: string; + run_status: string; + buffered_events?: unknown[]; + events_replayed?: number; + message: string; +} + +export interface ReconnectErrorMessage { + type: 'reconnect_error'; + error: string; + run_id?: string; +} + +export interface ReplayStartMessage { + type: 'replay_start'; + count: number; +} + +export interface ReplayEndMessage { + type: 'replay_end'; + count: number; +} + +export interface SessionExpiredMessage { + type: 'session_expired'; + reason: string; +} + export type WebSocketMessage = | LLMChunk | LLMResponse @@ -438,6 +490,7 @@ export type WebSocketMessage = | AgentStatus | ErrorMessage | RunReady + | RunStopped | AvailableToolsetsResponse | LLMRequestParams | LLMStreamStarted @@ -452,7 +505,14 @@ export type WebSocketMessage = | ViewModelUpdateFailed | TurnsSync | ProjectStructureUpdated - | TokenUsageUpdate; + | TokenUsageUpdate + | PingMessage + | HeartbeatAckMessage + | ReconnectedMessage + | ReconnectErrorMessage + | ReplayStartMessage + | ReplayEndMessage + | SessionExpiredMessage; export interface DispatchHistoryEntry { dispatch_instance_id: string; @@ -475,19 +535,25 @@ class SessionStore { isConnected: boolean = false; isConnecting: boolean = false; error: string | null = null; - isResuming: boolean = false; + isResuming: boolean = false; private maxRetryAttempts: number = 3; private retryDelayMs: number = 1000; // 1-second delay useLLMChunk = true; currentlySubscribedRunId: string | null = null; + // Session resilience + private sessionManager: SessionManager; + isReconnecting: boolean = false; + reconnectRunId: string | null = null; + private lastEventId: number = 0; + // ViewModel/Sync state flowStructure: FlowViewModel | null = null; kanbanViewModel: KanbanViewModel | null = null; timelineViewModel: TimelineViewModel | null = null; streamingContent = new Map(); viewErrors = new Map(); - + // New: Track if waiting for a new ViewModel isWaitingForNewViewModel: boolean = false; @@ -519,6 +585,26 @@ class SessionStore { workModules: Record> = {}; constructor() { + // Initialize session manager with callbacks + this.sessionManager = getSessionManager({ + onSessionExpired: () => { + console.warn('[SessionStore] Session expired - clearing state'); + runInAction(() => { + this.error = 'Session expired. Please refresh the page.'; + this.isConnected = false; + }); + this.cleanup(); + }, + onReconnectNeeded: (runId: string) => { + console.log('[SessionStore] Reconnection needed for run:', runId); + this.attemptReconnection(runId); + }, + onHeartbeatFailed: () => { + console.warn('[SessionStore] Heartbeat failed - attempting reconnection'); + this.handleDisconnection(); + }, + }); + makeAutoObservable(this, { chatHistoryTurns: computed, activityStreamTurns: computed @@ -563,8 +649,8 @@ class SessionStore { get chatHistoryTurns(): Turn[] { if (!this.turns) return []; // Only include 'User' and 'Partner' turns - return this.turns.filter(turn => - turn.agent_info?.agent_id.includes('User') || + return this.turns.filter(turn => + turn.agent_info?.agent_id.includes('User') || turn.agent_info?.agent_id.includes('Partner') ); } @@ -572,8 +658,8 @@ class SessionStore { get activityStreamTurns(): Turn[] { if (!this.turns) return []; // Exclude 'User' and 'Partner' turns - return this.turns.filter(turn => - !turn.agent_info?.agent_id.includes('User') && + return this.turns.filter(turn => + !turn.agent_info?.agent_id.includes('User') && !turn.agent_info?.agent_id.includes('Partner') ); } @@ -615,7 +701,7 @@ class SessionStore { get isSystemRunning() { // Based on the new Turn model, we check if there are any running Turns - return this.ws?.readyState === WebSocket.OPEN && + return this.ws?.readyState === WebSocket.OPEN && this.turns.some(turn => turn.status === 'running'); } @@ -629,7 +715,7 @@ class SessionStore { const isRunning = runTurns.some(t => t.status === 'running'); // If any Turn's LLM interaction is in progress, it is considered that streaming output has started - const isStreamStarted = runTurns.some(t => + const isStreamStarted = runTurns.some(t => t.agent_info.agent_id.includes('Partner') && t.llm_interaction?.status === 'running' ); @@ -644,25 +730,46 @@ class SessionStore { runInAction(() => { this.isConnecting = true; + this.error = null; }); try { - const data = await ProjectService.createSession(); - + // Use session manager to get or create session (handles reconnection) + const { tokens, isReconnect, reconnectInfo } = await this.sessionManager.getOrCreateSession(); + runInAction(() => { - this.sessionId = data.session_id; + this.sessionId = tokens.session_id; + this.isReconnecting = isReconnect; + this.reconnectRunId = reconnectInfo?.runId ?? null; }); - await this.connectWebSocket(data.session_id); + await this.connectWebSocket(tokens.session_id); + + // If reconnecting, send reconnect message + if (isReconnect && reconnectInfo) { + console.log('[SessionStore] Sending reconnect message for run:', reconnectInfo.runId); + this.sessionManager.sendReconnectMessage( + this.ws!, + reconnectInfo.runId, + reconnectInfo.lastEventId + ); + } + + // Start client heartbeat + if (this.ws) { + this.sessionManager.startHeartbeat(this.ws); + } runInAction(() => { this.isConnected = true; - console.log("WebSocket connection established successfully."); + console.log("WebSocket connection established successfully.", { isReconnect }); }); } catch (error) { console.error('Failed to initialize session and connect WebSocket:', error); runInAction(() => { this.isConnected = false; + this.isReconnecting = false; + this.reconnectRunId = null; // Provide a more user-friendly error message if (error instanceof Error && error.message === 'Failed to fetch') { this.error = 'Unable to connect to server. Please check your network connection and try again.'; @@ -680,7 +787,7 @@ class SessionStore { async connectWebSocket(sessionId: string) { return new Promise((resolve, reject) => { const ws = new WebSocket(`${config.ws.url}${config.ws.endpoint}/${sessionId}`); - + ws.onopen = () => { this.ws = ws; resolve(ws); @@ -693,12 +800,15 @@ class SessionStore { ws.onerror = (error) => { console.error('WebSocket error:', error); - this.error = 'Connection error. Please try again.'; + runInAction(() => { + this.error = 'Connection error. Please try again.'; + }); reject(error); }; - ws.onclose = () => { - this.ws = null; + ws.onclose = (event) => { + console.log('[SessionStore] WebSocket closed', { code: event.code, reason: event.reason }); + this.handleDisconnection(); }; }); } @@ -706,6 +816,77 @@ class SessionStore { handleWebSocketMessage(data: WebSocketMessage) { // console.log('Received message:', data.type, data); switch (data.type) { + // --- Session Resilience Messages --- + case 'ping': { + // Server heartbeat - respond with pong including sync data + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ + type: 'pong', + timestamp: (data as { timestamp?: string }).timestamp, + lastEventId: this.lastEventId, + clientTime: Date.now(), + })); + } + break; + } + case 'heartbeat_ack': { + // Handle server acknowledgment of client heartbeat + const ack = data as unknown as { timestamp: number; serverTime: string; sessionValid: boolean }; + this.sessionManager.handleHeartbeatAck(ack); + break; + } + case 'reconnected': { + // Successfully reconnected to a run + const reconnectData = data as unknown as { + run_id: string; + run_status: string; + buffered_events?: unknown[]; + events_replayed?: number; + message?: string; + }; + console.log('[SessionStore] Reconnected to run:', reconnectData); + runInAction(() => { + this.isReconnecting = false; + this.reconnectRunId = null; + // Subscribe to views for the reconnected run + if (reconnectData.run_id) { + this._subscribeToAllViews(reconnectData.run_id); + } + }); + break; + } + case 'reconnect_error': { + const errorData = data as unknown as { error: string; run_id?: string }; + console.error('[SessionStore] Reconnect failed:', errorData.error); + runInAction(() => { + this.isReconnecting = false; + this.reconnectRunId = null; + // Clear stored run info since reconnection failed + this.sessionManager.updateRunInfo('', undefined); + }); + break; + } + case 'replay_start': { + console.log('[SessionStore] Event replay starting'); + break; + } + case 'replay_end': { + const replayData = data as unknown as { run_id: string; events_replayed: number }; + console.log('[SessionStore] Event replay complete:', replayData.events_replayed, 'events'); + break; + } + case 'session_expired': { + const expiredData = data as unknown as { reason: string }; + console.warn('[SessionStore] Session expired:', expiredData.reason); + runInAction(() => { + this.error = 'Session expired. Please refresh the page.'; + this.isConnected = false; + }); + this.cleanup(); + break; + } + // --- End Session Resilience Messages --- + case 'turns_sync': { runInAction(() => { this.turns = data.data.turns; @@ -731,6 +912,15 @@ class SessionStore { this.runCreationPromises.delete(request_id); } this.runCreatedCounter += 1; + // Track the run for reconnection + this.sessionManager.updateRunInfo(run_id); + break; + } + case 'run_stopped': { + const { run_id, reason } = (data as RunStopped).data; + console.log(`Run ${run_id} stopped: ${reason}`); + // Run stays in memory on backend - no frontend tracking needed + // User can immediately send a new message to continue break; } case 'error': { @@ -746,7 +936,7 @@ class SessionStore { console.log('View Model Update Received:', data); const { view_name, model } = (data as ViewModelUpdate).data; this.viewErrors.set(view_name, null); - + // Reset waiting state (first ViewModel has arrived) if (this.isWaitingForNewViewModel) { runInAction(() => { @@ -754,11 +944,11 @@ class SessionStore { console.log('🎯 First ViewModel received, clearing waiting state'); }); } - + if (view_name === 'flow_view') { // Do not update turns from here anymore const newFlow = model as FlowViewModel; - + // Create a map of stream IDs from the current (old) flow structure // to preserve content of completed streams. const oldStreamIds = new Map(); @@ -783,7 +973,7 @@ class SessionStore { } } }); - + this.flowStructure = newFlow; } else if (view_name === 'kanban_view') { this.kanbanViewModel = model as KanbanViewModel; @@ -1031,12 +1221,12 @@ class SessionStore { this.ws!.send(JSON.stringify(payload)); return new Promise((resolve, reject) => { - this.runCreationPromises.set(requestId, { + this.runCreationPromises.set(requestId, { resolve: (returnedRunId: string) => { this._subscribeToAllViews(returnedRunId); resolve(returnedRunId); }, - reject + reject }); }); } @@ -1062,7 +1252,7 @@ class SessionStore { } }; this.ws!.send(JSON.stringify(payload)); - + return new Promise((resolve, reject) => { this.runCreationPromises.set(requestId, { resolve, reject }); setTimeout(() => { @@ -1149,15 +1339,127 @@ class SessionStore { return this.flowStructure.nodes.find(n => n.id === nodeId); } + // --- Session Resilience Methods --- + + /** + * Handle WebSocket disconnection - attempt reconnection if appropriate. + */ + private handleDisconnection() { + runInAction(() => { + this.ws = null; + this.isConnected = false; + }); + + // Stop client heartbeat + this.sessionManager.stopHeartbeat(); + + // Check if we can reconnect + const session = this.sessionManager.loadSession(); + if (session?.runId && session.runId.trim() !== '') { + console.log('[SessionStore] Disconnection detected with active run, will attempt reconnection'); + // Don't clear session - backend has grace period + this.attemptReconnection(session.runId); + } else { + console.log('[SessionStore] Disconnection detected, no active run to reconnect'); + } + } + + /** + * Attempt to reconnect to an active run. + */ + private async attemptReconnection(runId: string) { + if (!runId || runId.trim() === '') { + console.warn('[SessionStore] Cannot reconnect: runId is empty'); + return; + } + + if (this.isReconnecting) { + console.log('[SessionStore] Already attempting reconnection'); + return; + } + + runInAction(() => { + this.isReconnecting = true; + this.reconnectRunId = runId; + }); + + try { + // Check if run is still reconnectable + const runStatus = await this.sessionManager.getRunStatus(runId); + + if (!runStatus.can_reconnect) { + console.log('[SessionStore] Run is no longer reconnectable:', runStatus); + runInAction(() => { + this.isReconnecting = false; + this.reconnectRunId = null; + }); + return; + } + + // Get new session (will reuse fingerprint cookie) + const tokens = await this.sessionManager.createSession(); + + runInAction(() => { + this.sessionId = tokens.session_id; + }); + + // Connect WebSocket + await this.connectWebSocket(tokens.session_id); + + // Send reconnect message + const session = this.sessionManager.loadSession(); + this.sessionManager.sendReconnectMessage( + this.ws!, + runId, + session?.lastEventId + ); + + // Start client heartbeat + if (this.ws) { + this.sessionManager.startHeartbeat(this.ws); + } + + runInAction(() => { + this.isConnected = true; + console.log('[SessionStore] Reconnection WebSocket established'); + }); + } catch (error) { + console.error('[SessionStore] Reconnection failed:', error); + runInAction(() => { + this.isReconnecting = false; + this.reconnectRunId = null; + this.error = 'Reconnection failed. Please refresh the page.'; + }); + } + } + + /** + * Update the last event ID for reconnection tracking. + */ + trackEventId(eventId: number) { + this.lastEventId = eventId; + this.sessionManager.updateLastEventId(eventId); + } + + // --- End Session Resilience Methods --- + cleanup() { this.stopStreamingProcessor(); + this.sessionManager.stopHeartbeat(); + this.sessionManager.stopAutoRefresh(); + if (this.ws) { console.log('Closing WebSocket connection'); this.ws.close(); this.ws = null; + } + + runInAction(() => { this.isConnected = false; this.isConnecting = false; - } + this.isReconnecting = false; + this.reconnectRunId = null; + }); } // endregion } diff --git a/frontend/components/layout/AppSidebar.tsx b/frontend/components/layout/AppSidebar.tsx index 7076484..2cf4864 100644 --- a/frontend/components/layout/AppSidebar.tsx +++ b/frontend/components/layout/AppSidebar.tsx @@ -143,7 +143,7 @@ export const AppSidebar = observer(function AppSidebar() { setModelProviders(data.modelProviders) setGeneralSettings(data.generalSettings || {}) setAboutInfo(data.aboutInfo || null) - + // Initialize API Keys const keys: Record = {} Object.values(data.modelProviders).forEach((provider: ModelProvider) => { @@ -198,7 +198,7 @@ export const AppSidebar = observer(function AppSidebar() { value } })) - + // API call to save settings can be made here console.log(`Updating setting ${settingId} to:`, value) } @@ -251,9 +251,14 @@ export const AppSidebar = observer(function AppSidebar() { // Create new project const handleCreateProject = async () => { - if (!newProjectName.trim()) return - + console.log('[DEBUG] handleCreateProject called, newProjectName:', JSON.stringify(newProjectName)) + if (!newProjectName.trim()) { + console.log('[DEBUG] newProjectName is empty, returning') + return + } + try { + console.log('[DEBUG] Calling projectStore.createProject with:', { name: newProjectName.trim() }) await projectStore.createProject({ name: newProjectName.trim() }) setNewProjectName("") setIsNewProjectOpen(false) @@ -275,9 +280,9 @@ export const AppSidebar = observer(function AppSidebar() { } return dbName; }; - + const currentProjectName = getCurrentProjectName(project.project_id, project.name); - + // Use setTimeout to ensure the DropdownMenu is fully closed before opening the Dialog setTimeout(() => { setEditingProject({ id: project.project_id, name: currentProjectName }) @@ -288,13 +293,13 @@ export const AppSidebar = observer(function AppSidebar() { const handleUpdateProject = async () => { if (!editingProject || !editProjectName.trim()) return - + try { console.log('AppSidebar updating:', editProjectName.trim()); - + // Only call the API, subsequent synchronization relies entirely on projectStore.updateProject() β†’ loadProjects() β†’ updateProjectsMap() await projectStore.updateProject(editingProject.id, { name: editProjectName.trim() }) - + setEditingProject(null) setEditProjectName("") setIsEditProjectOpen(false) @@ -307,23 +312,23 @@ export const AppSidebar = observer(function AppSidebar() { // Delete project const handleConfirmDeleteProject = async () => { if (!projectToDeleteId) return - + try { await projectStore.deleteProject(projectToDeleteId) - + // After successful deletion, check and clear related selection states const deletedProjectId = projectToDeleteId; - + // If the currently selected project is the one being deleted, clear the selection state if (selectionStore.selectedProject?.projectId === deletedProjectId) { selectionStore.clearSelection(); } - + // If the currently selected file belongs to the deleted project, also clear the selection state if (selectionStore.selectedFile?.projectId === deletedProjectId) { selectionStore.clearSelection(); } - + } catch (error) { console.error('Failed to delete project:', error) // Error hints can be added here @@ -345,7 +350,7 @@ export const AppSidebar = observer(function AppSidebar() { // Handle file selection - use router navigation const handleFileSelect = (run: { filename?: string; meta: { run_id?: string; description?: string } }, projectData: { project: { project_id: string; name: string } }, index: number) => { const runId = run.meta.run_id || `${projectData.project.project_id}-${index}`; - + if (runId) { // Navigate to the run page router.push(`/r?id=${runId}`); @@ -371,7 +376,7 @@ export const AppSidebar = observer(function AppSidebar() { const handleUpdateFileName = async () => { if (!editingFile || !editFileName.trim()) return - + try { await projectStore.renameRun(editingFile.runId, editFileName.trim()) setEditingFile(null) @@ -414,19 +419,19 @@ export const AppSidebar = observer(function AppSidebar() { const handleDragStart = (e: React.DragEvent, run: { filename?: string; meta: { run_id?: string; description?: string } }, projectId: string) => { const runId = run.meta.run_id const filename = run.filename || run.meta.description || 'Untitled' - + // If there is no run_id, it cannot be dragged if (!runId) { e.preventDefault() return } - + setDraggedFile({ runId, filename, fromProjectId: projectId }) - + e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', runId) } @@ -441,7 +446,7 @@ export const AppSidebar = observer(function AppSidebar() { const handleDragOver = (e: React.DragEvent, projectId: string) => { e.preventDefault() e.dataTransfer.dropEffect = 'move' - + // Cannot drag to the same project if (draggedFile && draggedFile.fromProjectId !== projectId) { setDragOverProject(projectId) @@ -457,11 +462,11 @@ export const AppSidebar = observer(function AppSidebar() { const handleDrop = async (e: React.DragEvent, toProjectId: string) => { e.preventDefault() setDragOverProject(null) - + if (!draggedFile || draggedFile.fromProjectId === toProjectId) { return } - + try { await projectStore.moveRun(draggedFile.runId, draggedFile.fromProjectId, toProjectId) // Clear selection state on success @@ -505,10 +510,10 @@ export const AppSidebar = observer(function AppSidebar() {
πŸ€–
- setNewProjectName(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleCreateProject()} @@ -554,10 +559,10 @@ export const AppSidebar = observer(function AppSidebar() {
πŸ€–
- setEditProjectName(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleUpdateProject()} @@ -613,10 +618,10 @@ export const AppSidebar = observer(function AppSidebar() {
πŸ“„
- setEditFileName(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleUpdateFileName()} @@ -669,7 +674,7 @@ export const AppSidebar = observer(function AppSidebar() { // Default Project always comes first if (a.project.project_id === 'default') return -1; if (b.project.project_id === 'default') return 1; - + // Other projects are sorted by created_at in descending order (newest first) const dateA = new Date(a.project.created_at); const dateB = new Date(b.project.created_at); @@ -686,12 +691,12 @@ export const AppSidebar = observer(function AppSidebar() { } return dbName; }; - + const displayProjectName = getProjectDisplayName( - projectData.project.project_id, + projectData.project.project_id, projectData.project.name ); - + return ( - handleProjectSelect(projectData.project)} className={`w-full relative hover:bg-accent transition-all duration-200 ${ dragOverProject === projectData.project.project_id ? 'bg-blue-100 border-2 border-blue-300 border-dashed scale-[1.02]' : '' @@ -734,7 +739,7 @@ export const AppSidebar = observer(function AppSidebar() { Edit Project - triggerDeleteProject(projectData.project.project_id)} > @@ -750,25 +755,32 @@ export const AppSidebar = observer(function AppSidebar() { {projectData.runs - .slice().sort((a, b) => { + .slice() + // Deduplicate runs by run_id (keep first occurrence) + .filter((run, index, self) => { + const runId = run.meta?.run_id; + if (!runId) return true; // Keep runs without run_id + return self.findIndex(r => r.meta?.run_id === runId) === index; + }) + .sort((a, b) => { // Files are sorted by created in descending order (newest first) const getCreated = (run: { meta?: { created?: string; [key: string]: unknown } }) => { const meta = run.meta || {}; return meta.created || null; }; - + const timeA = getCreated(a); const timeB = getCreated(b); - + // If both have created, sort by time in descending order if (timeA && timeB) { return new Date(timeB).getTime() - new Date(timeA).getTime(); } - + // Those with created come first if (timeA && !timeB) return -1; if (!timeA && timeB) return 1; - + // If neither has created, maintain original order return 0; }) @@ -780,16 +792,16 @@ export const AppSidebar = observer(function AppSidebar() { const displayNameFromMeta = typeof run.meta?.display_name === 'string' ? run.meta.display_name : null; const filename = run.filename; const description = run.meta?.description; - + const fullName = displayNameFromMeta || filename || description || 'Untitled'; - + // Improved file name display logic, remove .iic extension const displayFileName = removeIicExtension(fullName); - + return (
- {displayFileName} - + {/* File action icons */}
+
*/} {/* Settings Dialog */} @@ -938,7 +950,7 @@ export const AppSidebar = observer(function AppSidebar() { })} - + {/* Right Content */}
{activeSettingTab === 'model-provider' && ( @@ -947,7 +959,7 @@ export const AppSidebar = observer(function AppSidebar() {

Receive emails about new products, features, and more.

- + {settingsLoading ? (
Loading...
@@ -1012,14 +1024,14 @@ export const AppSidebar = observer(function AppSidebar() { )}
)} - + {activeSettingTab === 'general-settings' && (

General Settings

Receive emails about new products, features, and more.

- + {settingsLoading ? (
Loading...
@@ -1032,7 +1044,7 @@ export const AppSidebar = observer(function AppSidebar() {

{setting.label}

{setting.description}

- +
{setting.type === 'switch' ? ( )} - + {activeSettingTab === 'about' && (

About

Receive emails about new products, features, and more.

- + {settingsLoading ? (
Loading...
@@ -1081,8 +1093,8 @@ export const AppSidebar = observer(function AppSidebar() {
- {aboutInfo.appInfo.name} ) -}); +}); diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..63b9b63 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,61 @@ +/** @type {import('jest').Config} */ +const config = { + // Test environment + testEnvironment: 'jsdom', + + // Setup files + setupFilesAfterEnv: ['/jest.setup.ts'], + + // Module resolution + moduleNameMapper: { + // Handle CSS imports (with CSS modules) + '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy', + + // Handle CSS imports (without CSS modules) + '^.+\\.(css|sass|scss)$': '/__mocks__/styleMock.js', + + // Handle image imports + '^.+\\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i': '/__mocks__/fileMock.js', + + // Handle module aliases (matching tsconfig paths) + '^@/(.*)$': '/$1', + }, + + // Transform files + transform: { + // Use ts-jest for ts/tsx files + '^.+\\.(ts|tsx)$': ['ts-jest', { + tsconfig: 'tsconfig.json', + }], + }, + + // Test patterns + testMatch: [ + '**/__tests__/**/*.(test|spec).(ts|tsx|js|jsx)', + '**/*.(test|spec).(ts|tsx|js|jsx)', + ], + + // Coverage configuration + collectCoverageFrom: [ + 'lib/**/*.{ts,tsx}', + 'app/**/*.{ts,tsx}', + 'components/**/*.{ts,tsx}', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/__tests__/**', + ], + + // Ignore patterns + testPathIgnorePatterns: [ + '/node_modules/', + '/.next/', + ], + + // Module file extensions + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + + // Verbose output + verbose: true, +}; + +module.exports = config; diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts new file mode 100644 index 0000000..19a3396 --- /dev/null +++ b/frontend/jest.setup.ts @@ -0,0 +1,40 @@ +import '@testing-library/jest-dom'; + +// Mock next/navigation +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + push: jest.fn(), + replace: jest.fn(), + prefetch: jest.fn(), + back: jest.fn(), + forward: jest.fn(), + }), + useSearchParams: () => new URLSearchParams(), + usePathname: () => '/', +})); + +// Mock next/image +jest.mock('next/image', () => ({ + __esModule: true, + default: (props: any) => { + const { src, alt, ...rest } = props; + // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text + return { + $$typeof: Symbol.for('react.element'), + type: 'img', + props: { src, alt, ...rest }, + key: null, + ref: null, + }; + }, +})); + +// Suppress console.log in tests (keep errors) +beforeAll(() => { + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 7188733..f13527a 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -7,27 +7,49 @@ interface Metadata { interface SessionResponse { session_id: string; + jwt_token: string; + refresh_token: string; + expires_in: number; + status: string; +} + +interface RefreshResponse { + jwt_token: string; + refresh_token: string; + expires_in: number; +} + +export interface RunStatusResponse { run_id: string; - ws_url: string; + exists: boolean; + state?: string; + can_reconnect: boolean; + connected_at?: string; + disconnected_at?: string; + grace_period_expires?: string; + buffered_events?: number; + last_checkpoint?: string; + message?: string; } export class ProjectService { - // Generic fetch method + // Generic fetch method with credentials private static async fetchApi(endpoint: string, options?: RequestInit): Promise { const url = `${config.api.baseUrl}${endpoint}` - + const response = await fetch(url, { headers: { 'Content-Type': 'application/json', ...options?.headers, }, + credentials: 'include', // Include cookies for fingerprint ...options, }) - + if (!response.ok) { throw new Error(`API Error: ${response.status} ${response.statusText}`) } - + return response.json() } @@ -43,10 +65,13 @@ export class ProjectService { // Create new project static async createProject(data: CreateProjectRequest): Promise<{ message: string; data: Project }> { - return this.fetchApi<{ message:string; data: Project }>('/project', { + console.log('[DEBUG] ProjectService.createProject called with:', data) + const result = await this.fetchApi<{ message:string; data: Project }>('/project', { method: 'POST', body: JSON.stringify(data), }) + console.log('[DEBUG] createProject result:', result) + return result } // Update project @@ -70,14 +95,27 @@ export class ProjectService { return this.fetchApi(`/metadata${query}`) } - // Create session - static async createSession(): Promise { + // Create session (with JWT tokens) + static async createSession(projectId: string = 'default'): Promise { return this.fetchApi('/session', { method: 'POST', - body: JSON.stringify({}), + body: JSON.stringify({ project_id: projectId }), }) } + // Refresh session tokens + static async refreshSession(refreshToken: string): Promise { + return this.fetchApi('/session/refresh', { + method: 'POST', + body: JSON.stringify({ refresh_token: refreshToken }), + }) + } + + // Get run connection status + static async getRunStatus(runId: string): Promise { + return this.fetchApi(`/run/${runId}/status`) + } + // Update run metadata static async updateRun(runId: string, metadata: Record): Promise<{ message: string }> { return this.fetchApi<{ message: string }>(`/run/${runId}`, { @@ -87,15 +125,15 @@ export class ProjectService { } // Rename run - static async renameRun(runId: string, newName: string): Promise<{ - message: string; - old_filename: string; - new_filename: string + static async renameRun(runId: string, newName: string): Promise<{ + message: string; + old_filename: string; + new_filename: string }> { - return this.fetchApi<{ - message: string; - old_filename: string; - new_filename: string + return this.fetchApi<{ + message: string; + old_filename: string; + new_filename: string }>(`/run/${runId}/name`, { method: 'PUT', body: JSON.stringify({ new_name: newName }), @@ -110,19 +148,19 @@ export class ProjectService { } // Move run to another project - static async moveRun(runId: string, fromProjectId: string, toProjectId: string): Promise<{ - message: string; - old_filename: string; - new_filename: string; - source_project: string; - destination_project: string + static async moveRun(runId: string, fromProjectId: string, toProjectId: string): Promise<{ + message: string; + old_filename: string; + new_filename: string; + source_project: string; + destination_project: string }> { - return this.fetchApi<{ - message: string; - old_filename: string; - new_filename: string; - source_project: string; - destination_project: string + return this.fetchApi<{ + message: string; + old_filename: string; + new_filename: string; + source_project: string; + destination_project: string }>('/run/move', { method: 'POST', body: JSON.stringify({ @@ -132,4 +170,4 @@ export class ProjectService { }), }) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/lib/sessionManager.ts b/frontend/lib/sessionManager.ts new file mode 100644 index 0000000..2f07d95 --- /dev/null +++ b/frontend/lib/sessionManager.ts @@ -0,0 +1,595 @@ +/** + * Session Manager for WebSocket Session Resilience + * + * Implements: + * - JWT token lifecycle management with auto-refresh at 90% of lifespan + * - sessionStorage persistence for session survival across page refresh + * - Dual heartbeat support (server-initiated ping/pong + client-initiated heartbeat) + * - Reconnection flow for resuming active runs + * + * Security: + * - JWT tokens stored in sessionStorage (cleared on browser close) + * - HttpOnly fingerprint cookie for token binding (handled by browser automatically) + * - Token rotation on each refresh (single-use refresh tokens) + * + * See docs/architecture/session-resilience.md for full design documentation. + */ + +import { config } from '../app/config'; + +// ============================================================================= +// Types +// ============================================================================= + +export interface SessionTokens { + session_id: string; + jwt_token: string; + refresh_token: string; + expires_in: number; // seconds +} + +export interface StoredSession { + sessionId: string; + jwtToken: string; + refreshToken: string; + expiresAt: number; // timestamp ms + issuedAt: number; // timestamp ms + runId?: string; + lastEventId?: number; +} + +export interface RefreshResult { + jwt_token: string; + refresh_token: string; + expires_in: number; +} + +export interface RunStatus { + run_id: string; + exists: boolean; + state?: string; + can_reconnect: boolean; + grace_period_expires?: string; + buffered_events?: number; + message?: string; +} + +export interface HeartbeatConfig { + intervalMs: number; + timeoutMs: number; + maxMissed: number; +} + +// ============================================================================= +// Storage Keys +// ============================================================================= + +const STORAGE_KEY = 'cg_session'; +const AUTO_REFRESH_THRESHOLD = 0.9; // Refresh at 90% of lifespan + +// ============================================================================= +// Session Manager Class +// ============================================================================= + +export class SessionManager { + private refreshTimerId: ReturnType | null = null; + private heartbeatTimerId: ReturnType | null = null; + private missedHeartbeats = 0; + private lastHeartbeatAck: number | null = null; + private websocket: WebSocket | null = null; + + private heartbeatConfig: HeartbeatConfig = { + intervalMs: 20000, // 20 seconds (matches backend CLIENT_HEARTBEAT_INTERVAL_SECONDS) + timeoutMs: 10000, // 10 seconds + maxMissed: 2, + }; + + private onSessionExpired?: () => void; + private onReconnectNeeded?: (runId: string) => void; + private onHeartbeatFailed?: () => void; + + constructor(callbacks?: { + onSessionExpired?: () => void; + onReconnectNeeded?: (runId: string) => void; + onHeartbeatFailed?: () => void; + }) { + this.onSessionExpired = callbacks?.onSessionExpired; + this.onReconnectNeeded = callbacks?.onReconnectNeeded; + this.onHeartbeatFailed = callbacks?.onHeartbeatFailed; + } + + // --------------------------------------------------------------------------- + // Session Persistence + // --------------------------------------------------------------------------- + + /** + * Save session to sessionStorage. + */ + saveSession(tokens: SessionTokens, runId?: string, lastEventId?: number): void { + const now = Date.now(); + const session: StoredSession = { + sessionId: tokens.session_id, + jwtToken: tokens.jwt_token, + refreshToken: tokens.refresh_token, + expiresAt: now + tokens.expires_in * 1000, + issuedAt: now, + runId, + lastEventId, + }; + + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session)); + console.log('[SessionManager] Session saved to storage', { + sessionId: tokens.session_id, + expiresIn: tokens.expires_in, + }); + } catch (e) { + console.error('[SessionManager] Failed to save session:', e); + } + } + + /** + * Load session from sessionStorage. + */ + loadSession(): StoredSession | null { + try { + const stored = sessionStorage.getItem(STORAGE_KEY); + if (!stored) return null; + + const session: StoredSession = JSON.parse(stored); + + // Check if JWT is expired (with small buffer) + if (Date.now() >= session.expiresAt - 5000) { + console.log('[SessionManager] Stored session JWT expired'); + // Don't clear - we might be able to refresh + return session; + } + + return session; + } catch (e) { + console.error('[SessionManager] Failed to load session:', e); + return null; + } + } + + /** + * Update run info in stored session. + */ + updateRunInfo(runId: string, lastEventId?: number): void { + const session = this.loadSession(); + if (session) { + session.runId = runId; + if (lastEventId !== undefined) { + session.lastEventId = lastEventId; + } + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session)); + } catch (e) { + console.error('[SessionManager] Failed to update run info:', e); + } + } + } + + /** + * Update last event ID in stored session. + */ + updateLastEventId(lastEventId: number): void { + const session = this.loadSession(); + if (session) { + session.lastEventId = lastEventId; + try { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(session)); + } catch (e) { + console.error('[SessionManager] Failed to update lastEventId:', e); + } + } + } + + /** + * Clear stored session. + */ + clearSession(): void { + try { + sessionStorage.removeItem(STORAGE_KEY); + console.log('[SessionManager] Session cleared from storage'); + } catch (e) { + console.error('[SessionManager] Failed to clear session:', e); + } + + this.stopAutoRefresh(); + this.stopHeartbeat(); + } + + // --------------------------------------------------------------------------- + // Token Refresh + // --------------------------------------------------------------------------- + + /** + * Schedule auto-refresh at 90% of token lifespan. + */ + scheduleAutoRefresh(tokens: SessionTokens): void { + this.stopAutoRefresh(); + + const lifespanMs = tokens.expires_in * 1000; + const refreshDelayMs = lifespanMs * AUTO_REFRESH_THRESHOLD; + + this.refreshTimerId = setTimeout(async () => { + console.log('[SessionManager] Auto-refresh triggered at 90% lifespan'); + await this.performSilentRefresh(); + }, refreshDelayMs); + + console.log('[SessionManager] Auto-refresh scheduled', { + expiresIn: tokens.expires_in, + refreshIn: refreshDelayMs / 1000, + }); + } + + /** + * Stop auto-refresh timer. + */ + stopAutoRefresh(): void { + if (this.refreshTimerId) { + clearTimeout(this.refreshTimerId); + this.refreshTimerId = null; + } + } + + /** + * Perform silent token refresh. + */ + async performSilentRefresh(): Promise { + const session = this.loadSession(); + if (!session?.refreshToken) { + console.warn('[SessionManager] No refresh token available'); + this.onSessionExpired?.(); + return false; + } + + try { + const result = await this.refreshTokens(session.refreshToken); + + if (result) { + // Update stored session with new tokens + const updatedTokens: SessionTokens = { + session_id: session.sessionId, + jwt_token: result.jwt_token, + refresh_token: result.refresh_token, + expires_in: result.expires_in, + }; + + this.saveSession(updatedTokens, session.runId, session.lastEventId); + this.scheduleAutoRefresh(updatedTokens); + + console.log('[SessionManager] Silent refresh successful'); + return true; + } + } catch (e) { + console.error('[SessionManager] Silent refresh failed:', e); + } + + this.onSessionExpired?.(); + return false; + } + + /** + * Call refresh endpoint. + */ + private async refreshTokens(refreshToken: string): Promise { + const apiBase = config.api.baseUrl; + + const response = await fetch(`${apiBase}/session/refresh`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // Include cookies (fingerprint) + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + if (!response.ok) { + console.error('[SessionManager] Refresh request failed:', response.status); + return null; + } + + return response.json(); + } + + // --------------------------------------------------------------------------- + // Client Heartbeat + // --------------------------------------------------------------------------- + + /** + * Start client heartbeat sender. + */ + startHeartbeat(websocket: WebSocket): void { + this.stopHeartbeat(); + this.websocket = websocket; + this.missedHeartbeats = 0; + this.lastHeartbeatAck = Date.now(); + + this.heartbeatTimerId = setInterval(() => { + this.sendHeartbeat(); + }, this.heartbeatConfig.intervalMs); + + console.log('[SessionManager] Client heartbeat started', { + interval: this.heartbeatConfig.intervalMs, + }); + } + + /** + * Stop client heartbeat sender. + */ + stopHeartbeat(): void { + if (this.heartbeatTimerId) { + clearInterval(this.heartbeatTimerId); + this.heartbeatTimerId = null; + } + this.websocket = null; + } + + /** + * Send heartbeat to server. + */ + private sendHeartbeat(): void { + if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) { + return; + } + + const session = this.loadSession(); + const now = Date.now(); + + // Check if we've missed heartbeats + if (this.lastHeartbeatAck) { + const timeSinceAck = now - this.lastHeartbeatAck; + if (timeSinceAck > this.heartbeatConfig.intervalMs + this.heartbeatConfig.timeoutMs) { + this.missedHeartbeats++; + console.warn('[SessionManager] Heartbeat ack missed', { + missed: this.missedHeartbeats, + timeSinceAck, + }); + + if (this.missedHeartbeats >= this.heartbeatConfig.maxMissed) { + console.error('[SessionManager] Max heartbeats missed - triggering reconnection'); + this.onHeartbeatFailed?.(); + return; + } + } + } + + const heartbeat = { + type: 'heartbeat', + timestamp: now, + sessionId: session?.sessionId, + runId: session?.runId, + }; + + try { + this.websocket.send(JSON.stringify(heartbeat)); + } catch (e) { + console.error('[SessionManager] Failed to send heartbeat:', e); + } + } + + /** + * Handle heartbeat acknowledgment from server. + */ + handleHeartbeatAck(ack: { timestamp: number; serverTime: string; sessionValid: boolean }): void { + this.lastHeartbeatAck = Date.now(); + this.missedHeartbeats = 0; + + if (!ack.sessionValid) { + console.warn('[SessionManager] Server reports session invalid'); + this.onSessionExpired?.(); + } + } + + // --------------------------------------------------------------------------- + // Reconnection + // --------------------------------------------------------------------------- + + /** + * Check if there's a session that can be reconnected. + */ + async checkForReconnection(): Promise<{ canReconnect: boolean; runId?: string; runStatus?: RunStatus }> { + const session = this.loadSession(); + + if (!session?.runId) { + return { canReconnect: false }; + } + + try { + const runStatus = await this.getRunStatus(session.runId); + + if (runStatus.can_reconnect) { + return { + canReconnect: true, + runId: session.runId, + runStatus, + }; + } + + return { canReconnect: false, runStatus }; + } catch (e) { + console.error('[SessionManager] Failed to check run status:', e); + return { canReconnect: false }; + } + } + + /** + * Get run connection status from server. + */ + async getRunStatus(runId: string): Promise { + const apiBase = config.api.baseUrl; + + const response = await fetch(`${apiBase}/run/${runId}/status`, { + method: 'GET', + credentials: 'include', + }); + + if (!response.ok) { + throw new Error(`Failed to get run status: ${response.status}`); + } + + return response.json(); + } + + /** + * Send reconnect message over WebSocket. + */ + sendReconnectMessage(websocket: WebSocket, runId: string, lastEventId?: number): void { + if (!runId || runId.trim() === '') { + console.error('[SessionManager] Cannot send reconnect message: runId is empty'); + return; + } + + const message = { + type: 'reconnect', + run_id: runId, + last_event_id: lastEventId ?? 0, + }; + + websocket.send(JSON.stringify(message)); + console.log('[SessionManager] Reconnect message sent', { runId, lastEventId }); + } + + // --------------------------------------------------------------------------- + // Session Creation + // --------------------------------------------------------------------------- + + /** + * Create a new session. + */ + async createSession(projectId: string = 'default'): Promise { + const apiBase = config.api.baseUrl; + + const response = await fetch(`${apiBase}/session`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', // Include cookies for fingerprint + body: JSON.stringify({ project_id: projectId }), + }); + + if (!response.ok) { + throw new Error(`Failed to create session: ${response.status}`); + } + + const tokens: SessionTokens = await response.json(); + + // Save to storage and schedule refresh + this.saveSession(tokens); + this.scheduleAutoRefresh(tokens); + + return tokens; + } + + /** + * Get or create a session, checking for reconnection possibilities. + */ + async getOrCreateSession(projectId: string = 'default'): Promise<{ + tokens: SessionTokens; + isReconnect: boolean; + reconnectInfo?: { runId: string; lastEventId?: number }; + }> { + // Check for existing session + const existingSession = this.loadSession(); + + if (existingSession) { + // Check if JWT is still valid + const isExpired = Date.now() >= existingSession.expiresAt - 5000; + + if (isExpired) { + // Try to refresh + const refreshed = await this.performSilentRefresh(); + if (!refreshed) { + // Create new session + const tokens = await this.createSession(projectId); + return { tokens, isReconnect: false }; + } + + // Reload refreshed session + const refreshedSession = this.loadSession()!; + const tokens: SessionTokens = { + session_id: refreshedSession.sessionId, + jwt_token: refreshedSession.jwtToken, + refresh_token: refreshedSession.refreshToken, + expires_in: Math.floor((refreshedSession.expiresAt - Date.now()) / 1000), + }; + + // Check for reconnection + if (refreshedSession.runId) { + const { canReconnect } = await this.checkForReconnection(); + if (canReconnect) { + return { + tokens, + isReconnect: true, + reconnectInfo: { + runId: refreshedSession.runId, + lastEventId: refreshedSession.lastEventId, + }, + }; + } + } + + return { tokens, isReconnect: false }; + } + + // JWT is still valid + const tokens: SessionTokens = { + session_id: existingSession.sessionId, + jwt_token: existingSession.jwtToken, + refresh_token: existingSession.refreshToken, + expires_in: Math.floor((existingSession.expiresAt - Date.now()) / 1000), + }; + + // Schedule refresh if not already scheduled + this.scheduleAutoRefresh(tokens); + + // Check for reconnection + if (existingSession.runId) { + const { canReconnect } = await this.checkForReconnection(); + if (canReconnect) { + return { + tokens, + isReconnect: true, + reconnectInfo: { + runId: existingSession.runId, + lastEventId: existingSession.lastEventId, + }, + }; + } + } + + return { tokens, isReconnect: false }; + } + + // No existing session - create new + const tokens = await this.createSession(projectId); + return { tokens, isReconnect: false }; + } +} + +// ============================================================================= +// Singleton Instance +// ============================================================================= + +let sessionManagerInstance: SessionManager | null = null; + +export function getSessionManager(callbacks?: { + onSessionExpired?: () => void; + onReconnectNeeded?: (runId: string) => void; + onHeartbeatFailed?: () => void; +}): SessionManager { + if (!sessionManagerInstance) { + sessionManagerInstance = new SessionManager(callbacks); + } + return sessionManagerInstance; +} + +export function resetSessionManager(): void { + if (sessionManagerInstance) { + sessionManagerInstance.clearSession(); + sessionManagerInstance = null; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index addf49f..4fa9eb1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "common-ground-webview", "version": "0.1.0", + "license": "Apache-2.0", "dependencies": { "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-collapsible": "^1.1.11", @@ -31,6 +32,7 @@ "mobx": "^6.13.5", "mobx-react-lite": "^4.1.0", "next": "^15.3.2", + "next-themes": "^0.4.6", "open-graph-scraper": "^6.10.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -39,6 +41,7 @@ "reactflow": "^11.11.4", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", + "sonner": "^2.0.5", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "uuid": "^11.1.0", @@ -46,17 +49,31 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.1", "@types/d3-flextree": "^2.1.4", + "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.1", "eslint": "^8", "eslint-config-next": "^15.3.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8", "tailwindcss": "^3.4.16", + "ts-jest": "^29.2.5", "typescript": "^5" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -69,465 +86,738 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "bin": { + "json5": "lib/cli.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">=10.10.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=12.22" + "node": ">=6.9.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.9.0" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "url": "https://opencollective.com/libvips" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.9.0" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, - "funding": { - "url": "https://opencollective.com/libvips" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz", + "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "dev": true, + "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@emnapi/wasi-threads": "1.0.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", + "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://opencollective.com/eslint" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.0.tgz", + "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.0.tgz", + "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", + "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", "cpu": [ "arm64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -536,20 +826,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "@img/sharp-libvips-darwin-arm64": "1.1.0" } }, - "node_modules/@img/sharp-linuxmusl-x64": { + "node_modules/@img/sharp-darwin-x64": { "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "resolved": "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", + "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", "cpu": [ "x64" ], "license": "Apache-2.0", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -558,1263 +848,1153 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "@img/sharp-libvips-darwin-x64": "1.1.0" } }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", "cpu": [ - "wasm32" + "arm64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "LGPL-3.0-or-later", "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, + "os": [ + "darwin" + ], "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", "cpu": [ - "ia32" + "x64" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "darwin" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", "cpu": [ - "x64" + "arm" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "win32" + "linux" ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", - "dev": true, - "license": "MIT", + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@next/env": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/env/-/env-15.3.2.tgz", - "integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==", - "license": "MIT" - }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", - "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "3.3.1" + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz", - "integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==", + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", "cpu": [ - "arm64" + "ppc64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz", - "integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==", + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", "cpu": [ - "x64" + "s390x" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ - "darwin" + "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz", - "integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==", + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", "cpu": [ - "arm64" + "x64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz", - "integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==", + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", - "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", "cpu": [ "x64" ], - "license": "MIT", + "license": "LGPL-3.0-or-later", "optional": true, "os": [ "linux" ], - "engines": { - "node": ">= 10" + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", - "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", + "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", "cpu": [ - "x64" + "arm" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 10" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz", - "integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==", + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", + "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", "cpu": [ "arm64" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">= 10" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz", - "integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==", + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", + "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", "cpu": [ - "x64" + "s390x" ], - "license": "MIT", + "license": "Apache-2.0", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": ">= 10" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", + "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", + "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 8" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", + "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", + "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@emnapi/runtime": "^1.4.0" }, "engines": { - "node": ">= 8" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@nolyfill/is-core-module": { - "version": "1.0.39", - "resolved": "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", - "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", - "dev": true, - "license": "MIT", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", + "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.4.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", + "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=14" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz", - "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz", - "integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==", - "license": "MIT", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", "dependencies": { - "@radix-ui/react-primitive": "2.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=12" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "ansi-regex": "^6.0.1" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=12" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.11", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", - "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", - "license": "MIT", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", - "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "p-try": "^2.0.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=6" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "p-limit": "^2.2.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=8" } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.14", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", - "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { + "node-notifier": { "optional": true } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@jest/core/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@types/node": "*", + "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { - "@types/react": { + "@types/node": { "optional": true }, - "@types/react-dom": { + "ts-node": { "optional": true } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.9", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", - "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", - "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "jest-get-type": "^29.6.3" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", - "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", - "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { + "node-notifier": { "optional": true } } }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@sinclair/typebox": "^0.27.8" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.15", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", - "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.10", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, - "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", - "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", + "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "@emnapi/core": "^1.4.0", + "@emnapi/runtime": "^1.4.0", + "@tybys/wasm-util": "^0.9.0" } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@next/env": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/env/-/env-15.3.2.tgz", + "integrity": "sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.2.tgz", + "integrity": "sha512-ijVRTXBgnHT33aWnDtmlG+LJD+5vhc9AKTJPquGG5NKXjpKNjc62woIhFtrAcWdBobt8kqjCoaJ0q6sDQoX7aQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "fast-glob": "3.3.1" } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz", + "integrity": "sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz", + "integrity": "sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz", + "integrity": "sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz", + "integrity": "sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz", + "integrity": "sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz", + "integrity": "sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz", + "integrity": "sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz", + "integrity": "sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 8" } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.6.tgz", + "integrity": "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", @@ -1831,13 +2011,16 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", "license": "MIT", "dependencies": { + "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { @@ -1855,9 +2038,9 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { @@ -1878,22 +2061,20 @@ } } }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.6", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", - "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.6", + "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -1910,14 +2091,13 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.8", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", - "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -1934,14 +2114,16 @@ } } }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", - "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "node_modules/@radix-ui/react-collection": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", + "integrity": "sha512-PbhRFK4lIEw9ADonj48tiYWzkllz81TM7KVYyyMMw2cwHO7D5h4XKEblL8NlaRisTK3QTe6tBEhDccFUryxHBQ==", "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -1958,37 +2140,29 @@ } } }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", - "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.2" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -1999,47 +2173,41 @@ } } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", - "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", "license": "MIT", "dependencies": { + "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", @@ -2056,13 +2224,17 @@ } } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2079,33 +2251,15 @@ } } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.4", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.4.tgz", - "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", "license": "MIT", "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.9", - "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.6", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.6", - "@radix-ui/react-portal": "1.1.8", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-slot": "1.2.2", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2122,31 +2276,14 @@ } } }, - "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2163,7 +2300,7 @@ } } }, - "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", @@ -2186,14 +2323,11 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -2204,19 +2338,17 @@ } } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.5", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", - "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2233,13 +2365,19 @@ } } }, - "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", @@ -2256,24 +2394,13 @@ } } }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", - "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.10", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.7", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -2290,40 +2417,30 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, "peerDependencies": { "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { "optional": true - }, - "@types/react-dom": { - "optional": true } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.10", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", - "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", + "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", @@ -2340,1468 +2457,4243 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", - "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", + "integrity": "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.4", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-select/-/react-select-2.2.4.tgz", + "integrity": "sha512-/OOm58Gil4Ev5zT8LyVzqfBcij4dTHYdeyuF5lMHZ2bIp0Lk9oETocYiJ5QC0dHekEQnK6L/FNJCceeb4AkZ6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.6", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.9", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.6", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.6", + "@radix-ui/react-portal": "1.1.8", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", + "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", + "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmmirror.com/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", + "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-flextree": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/d3-flextree/-/d3-flextree-2.1.4.tgz", + "integrity": "sha512-VcJAeoG51p0RBese55p/OHarbqe9QvQauzCkRAuORT3t9UOa+LzqCpTBMVYxBahyp4vAe1ML3sgeQKRU7Gv+KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-hierarchy": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.47", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.47.tgz", + "integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.21", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.21.tgz", + "integrity": "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", + "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/type-utils": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.32.1.tgz", + "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", + "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", + "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.32.1", + "@typescript-eslint/utils": "8.32.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.32.1.tgz", + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/visitor-keys": "8.32.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.32.1.tgz", + "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.32.1", + "@typescript-eslint/types": "8.32.1", + "@typescript-eslint/typescript-estree": "8.32.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.32.1", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.32.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", + "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", + "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", + "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", + "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", + "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", + "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", + "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", + "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", + "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.9" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "acorn": "^8.11.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=0.4.0" } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "debug": "4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">= 6.0.0" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" + "type-fest": "^0.21.3" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=8" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" + "color-convert": "^2.0.1" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=8" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", - "license": "MIT", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">= 8" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "tslib": "^2.0.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.2.tgz", - "integrity": "sha512-ORCmRUbNiZIv6uV5mhFrhsIKw4UX/N3syZtyqvry61tbGm4JlgQuSn0hk5TwCARsCjkcnuRkSdCE3xfb+ADHew==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.2" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, - "node_modules/@reactflow/background": { - "version": "11.3.14", - "resolved": "https://registry.npmmirror.com/@reactflow/background/-/background-11.3.14.tgz", - "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@reactflow/controls": { - "version": "11.2.14", - "resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz", - "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@reactflow/core": { - "version": "11.11.4", - "resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz", - "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3": "^7.4.0", - "@types/d3-drag": "^3.0.1", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "engines": { + "node": ">= 0.4" } }, - "node_modules/@reactflow/minimap": { - "version": "11.7.14", - "resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz", - "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.4.1" + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "node_modules/@reactflow/node-resizer": { - "version": "2.2.14", - "resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", - "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.4", - "d3-drag": "^3.0.0", - "d3-selection": "^3.0.0", - "zustand": "^4.4.1" + "possible-typed-array-names": "^1.0.0" }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@reactflow/node-toolbar": { - "version": "1.3.14", - "resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", - "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { - "@reactflow/core": "11.11.4", - "classcat": "^5.0.3", - "zustand": "^4.4.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.11.0", - "resolved": "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", - "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, - "license": "MIT" - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" + "engines": { + "node": ">= 0.4" } }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", - "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, "license": "MIT", "dependencies": { - "lodash.castarray": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.merge": "^4.6.2", - "postcss-selector-parser": "6.0.10" + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + "@babel/core": "^7.8.0" } }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "resolved": "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", - "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, - "license": "MIT", - "optional": true, + "license": "BSD-3-Clause", "dependencies": { - "tslib": "^2.4.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "license": "MIT" + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-selection": "*" + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", - "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", - "license": "MIT" + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", "dependencies": { - "@types/d3-selection": "*" + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@types/d3-dsv": "*" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/@types/d3-flextree": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/d3-flextree/-/d3-flextree-2.1.4.tgz", - "integrity": "sha512-VcJAeoG51p0RBese55p/OHarbqe9QvQauzCkRAuORT3t9UOa+LzqCpTBMVYxBahyp4vAe1ML3sgeQKRU7Gv+KA==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { - "@types/d3-hierarchy": "*" + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "@types/geojson": "*" + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" } }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-color": "*" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "@types/d3-time": "*" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-path": "*" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" + "engines": { + "node": ">=6" } }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "license": "MIT", - "dependencies": { - "@types/ms": "*" + "engines": { + "node": ">= 6" } }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" + "node_modules/caniuse-lite": { + "version": "1.0.30001761", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz", + "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", "license": "MIT", - "dependencies": { - "@types/estree": "*" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/unist": "*" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", "license": "MIT", - "dependencies": { - "@types/unist": "*" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/ms": { + "node_modules/character-entities-html4": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "20.17.47", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.47.tgz", - "integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==", - "dev": true, + "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", - "dependencies": { - "undici-types": "~6.19.2" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.21", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.21.tgz", - "integrity": "sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==", + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "node_modules/chardet": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", "license": "MIT" }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "license": "MIT" + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", - "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", - "dev": true, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/type-utils": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 8.10.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://paulmillr.com/funding/" }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "license": "MIT", "engines": { - "node": ">= 4" + "node": ">=8" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.32.1.tgz", - "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4" + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "node": ">=12" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", - "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=8" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", - "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.1", - "@typescript-eslint/utils": "8.32.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.32.1.tgz", - "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", - "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", + "optional": true, "dependencies": { - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/visitor-keys": "8.32.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "color-convert": "^2.0.1", + "color-string": "^1.9.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", + "optional": true, "dependencies": { - "balanced-match": "^1.0.0" + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.8" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", "engines": { "node": ">= 6" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.32.1.tgz", - "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", + "node_modules/create-jest/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.1", - "@typescript-eslint/types": "8.32.1", - "@typescript-eslint/typescript-estree": "8.32.1" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.1", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", - "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "node_modules/create-jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.1", - "eslint-visitor-keys": "^4.2.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 6" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", - "cpu": [ - "arm64" - ], + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", - "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", - "cpu": [ - "x64" - ], + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", - "cpu": [ - "arm" - ], + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", - "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", - "cpu": [ - "arm" - ], + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", - "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", - "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", - "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", - "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-flextree": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/d3-flextree/-/d3-flextree-2.1.2.tgz", + "integrity": "sha512-gJiHrx5uTTHq44bjyIb3xpbmmdZcWLYPKeO9EPVOq8EylMFOiH2+9sWqKAiQ4DcFuOZTAxPOQyv0Rnmji/g15A==", + "license": "WTFPL", + "dependencies": { + "d3-hierarchy": "^1.1.5" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", + "license": "BSD-3-Clause" }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", - "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.9" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=14.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", - "cpu": [ - "ia32" - ], + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-2-Clause" }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", - "cpu": [ - "x64" - ], + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/inspect-js" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">=8" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, "license": "MIT" }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "character-entities": "^2.0.0" }, - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" }, - "engines": { - "node": ">=10" + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -3810,19 +6702,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -3831,305 +6720,437 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" + "esutils": "^2.0.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6.0.0" } }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, "license": "MIT", + "peer": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ast-types-flow": { - "version": "0.0.8", - "resolved": "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", - "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", - "dev": true, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmmirror.com/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", - "dev": true, - "license": "MPL-2.0", + "node_modules/entities": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", + "license": "BSD-2-Clause", "engines": { - "node": ">=4" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "is-arrayish": "^0.2.1" } }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "node_modules/error-ex/node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { - "streamsearch": "^1.1.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=10.16.0" + "node": ">= 0.4" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -4138,888 +7159,884 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", + "node": ">=10" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" }, "engines": { - "node": ">=10" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optionalDependencies": { + "source-map": "~0.6.1" } }, - "node_modules/chardet": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/chardet/-/chardet-2.1.0.tgz", - "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", - "license": "MIT" - }, - "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, "license": "MIT", "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=18.17" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", + "node_modules/eslint-config-next": { + "version": "15.3.2", + "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.3.2.tgz", + "integrity": "sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==", + "dev": true, + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" + "@next/eslint-plugin-next": "15.3.2", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" }, "engines": { - "node": ">= 8.10.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://opencollective.com/eslint-import-resolver-typescript" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "debug": "^3.2.7" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" + "node": ">=4" }, - "funding": { - "url": "https://polar.sh/cva" + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "ms": "^2.1.1" } }, - "node_modules/cmdk": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", - "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-id": "^1.1.0", - "@radix-ui/react-primitive": "^2.0.2" + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" }, "peerDependencies": { - "react": "^18 || ^19 || ^19.0.0-rc", - "react-dom": "^18 || ^19 || ^19.0.0-rc" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" + "ms": "^2.1.1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "color-name": "~1.1.4" + "esutils": "^2.0.2" }, "engines": { - "node": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "esutils": "^2.0.2" }, "engines": { - "node": ">= 8" + "node": ">=0.10.0" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", - "license": "BSD-2-Clause", + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", "bin": { - "cssesc": "bin/cssesc" + "resolve": "bin/resolve" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", - "engines": { - "node": ">=12" + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" - } - }, - "node_modules/d3-flextree": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/d3-flextree/-/d3-flextree-2.1.2.tgz", - "integrity": "sha512-gJiHrx5uTTHq44bjyIb3xpbmmdZcWLYPKeO9EPVOq8EylMFOiH2+9sWqKAiQ4DcFuOZTAxPOQyv0Rnmji/g15A==", - "license": "WTFPL", - "dependencies": { - "d3-hierarchy": "^1.1.5" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/d3-hierarchy": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", - "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "d3-color": "1 - 3" + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, "engines": { - "node": ">=12" + "node": ">=4" } }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" + "estraverse": "^5.1.0" }, "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" + "node": ">=0.10" } }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=12" + "node": ">=4.0" } }, - "node_modules/damerau-levenshtein": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", - "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" + "node": ">=0.10.0" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.8.0" } }, - "node_modules/decode-named-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", - "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, "license": "MIT", "dependencies": { - "character-entities": "^2.0.0" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8.6.0" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "is-glob": "^4.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, "license": "Apache-2.0", - "optional": true, - "engines": { - "node": ">=8" + "dependencies": { + "bser": "2.1.1" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, "license": "MIT", "dependencies": { - "dequal": "^2.0.0" + "flat-cache": "^3.0.4" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=8" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { - "type": "github", - "url": "https://github.com/sponsors/fb55" + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "BSD-2-Clause" + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0" + "is-callable": "^1.2.7" }, "engines": { - "node": ">= 4" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" }, "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" }, "engines": { - "node": ">= 0.4" + "node": ">= 6" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/entities": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.0.tgz", - "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", - "license": "BSD-2-Clause", + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, "engines": { - "node": ">=0.12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-regex": "^1.2.1", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5028,103 +8045,210 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-define-property": { + "node_modules/get-nonce": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">= 0.4" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "type-fest": "^0.20.2" }, "engines": { - "node": ">= 0.4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "license": "MIT", "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5132,636 +8256,614 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escape-string-regexp": { + "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" }, - "bin": { - "eslint": "bin/eslint.js" + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-config-next": { - "version": "15.3.2", - "resolved": "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.3.2.tgz", - "integrity": "sha512-FerU4DYccO4FgeYFFglz0SnaKRe1ejXQrDb8kWUkTAg036YWi+jUsgg4sIGNCDhAsDITsZaL4MzBWKB6f4G1Dg==", - "dev": true, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.2", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "has-symbols": "^1.0.3" }, - "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", - "typescript": ">=3.3.1" + "engines": { + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" } }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "3.10.1", - "resolved": "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", - "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", - "dev": true, - "license": "ISC", + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", "dependencies": { - "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", - "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" + "@types/hast": "^3.0.0" }, "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", - "dev": true, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", - "dev": true, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", "license": "MIT", "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", - "hasown": "^2.0.2", - "is-core-module": "^2.15.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.0", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", - "tsconfig-paths": "^3.15.0" + "@types/hast": "^3.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.1" + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/html-encoding-sniffer/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.2", - "resolved": "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", - "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", - "dev": true, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", "dependencies": { - "aria-query": "^5.3.2", - "array-includes": "^3.1.8", - "array.prototype.flatmap": "^1.3.2", - "ast-types-flow": "^0.0.8", - "axe-core": "^4.10.0", - "axobject-query": "^4.1.0", - "damerau-levenshtein": "^1.0.8", - "emoji-regex": "^9.2.2", - "hasown": "^2.0.2", - "jsx-ast-utils": "^3.3.5", - "language-tags": "^1.0.9", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.1" - }, + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { - "node": ">=4.0" + "node": ">=0.12" }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", "dev": true, "license": "MIT", "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" }, "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "node": ">= 6" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "agent-base": "6", + "debug": "4" }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "engines": { + "node": ">= 6" } }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { + "node_modules/human-signals": { "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=10.17.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "harmony-reflect": "^1.4.6" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=4" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 4" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=4.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=0.8.19" } }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", "dependencies": { - "reusify": "^1.0.4" + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "has-bigints": "^1.0.2" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "binary-extensions": "^2.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=8" } }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, "engines": { "node": ">= 0.4" }, @@ -5769,80 +8871,48 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "hasown": "^2.0.2" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, "license": "MIT", "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" }, "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -5851,32 +8921,33 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", "license": "MIT", "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -5885,38 +8956,36 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" } }, - "node_modules/get-symbol-description": { + "node_modules/is-generator-function": { "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5925,78 +8994,107 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "is-extglob": "^2.1.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=0.12.0" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "license": "MIT", "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -6005,10 +9103,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6017,19 +9116,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -6037,37 +9132,46 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.0" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -6076,11 +9180,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, "engines": { "node": ">= 0.4" }, @@ -6088,14 +9196,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, "engines": { "node": ">= 0.4" }, @@ -6103,824 +9209,1090 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10" } }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10" } }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", - "license": "MIT", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=10" } }, - "node_modules/hast-util-to-parse5/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.4" } }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" + "@isaacs/cliui": "^8.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz", - "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.12" + "node": ">=10" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-cli/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=0.8.19" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/jest-cli/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/jest-cli/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "license": "ISC" - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "engines": { + "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "license": "MIT", "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "license": "MIT", "dependencies": { - "has-bigints": "^1.0.2" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.7.1" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=10" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/jiti": { @@ -6951,6 +10323,89 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6958,6 +10413,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7011,6 +10473,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -7031,6 +10503,16 @@ "node": ">=0.10" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", @@ -7093,6 +10575,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7137,6 +10626,50 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", @@ -7438,6 +10971,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", @@ -8044,6 +11584,26 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", @@ -8168,6 +11728,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/next": { "version": "15.3.2", "resolved": "https://registry.npmmirror.com/next/-/next-15.3.2.tgz", @@ -8222,6 +11789,16 @@ } } }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", @@ -8250,6 +11827,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8259,6 +11850,19 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", @@ -8271,6 +11875,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -8412,6 +12023,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open-graph-scraper": { "version": "6.10.0", "resolved": "https://registry.npmmirror.com/open-graph-scraper/-/open-graph-scraper-6.10.0.tgz", @@ -8495,6 +12122,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -8539,6 +12176,25 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", @@ -8663,6 +12319,75 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8791,6 +12516,58 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", @@ -8819,6 +12596,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", @@ -8829,6 +12619,30 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9026,6 +12840,20 @@ "node": ">=8.10.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -9151,6 +12979,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", @@ -9171,6 +13016,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -9191,6 +13059,16 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", @@ -9302,6 +13180,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", @@ -9533,6 +13424,43 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -9542,6 +13470,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -9552,6 +13491,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz", @@ -9559,6 +13505,29 @@ "dev": true, "license": "MIT" }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9567,6 +13536,20 @@ "node": ">=10.0.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", @@ -9794,6 +13777,29 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -9939,6 +13945,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "2.6.0", "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz", @@ -10058,6 +14071,21 @@ } } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", @@ -10131,6 +14159,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10143,6 +14178,35 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", @@ -10182,6 +14246,98 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -10214,6 +14370,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", @@ -10319,6 +14485,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -10441,6 +14621,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unrs-resolver": { "version": "1.7.2", "resolved": "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.7.2.tgz", @@ -10474,6 +14664,37 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", @@ -10484,6 +14705,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -10555,6 +14787,21 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", @@ -10597,6 +14844,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watch": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/watch/-/watch-0.13.0.tgz", @@ -10621,6 +14891,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -10642,6 +14922,20 @@ "node": ">=18" } }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -10756,6 +15050,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -10857,6 +15158,83 @@ "dev": true, "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.0.tgz", @@ -10869,6 +15247,57 @@ "node": ">= 14.6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 63cd3e4..57c84bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,10 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage" }, "dependencies": { "@radix-ui/react-avatar": "^1.1.10", @@ -50,14 +53,21 @@ }, "devDependencies": { "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.1", "@types/d3-flextree": "^2.1.4", + "@types/jest": "^29.5.14", "@types/node": "^20", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.1", "eslint": "^8", "eslint-config-next": "^15.3.2", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "postcss": "^8", "tailwindcss": "^3.4.16", + "ts-jest": "^29.2.5", "typescript": "^5" }, "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/frontend/yarn.lock b/frontend/yarn.lock index aaa001c..d77493d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,44 +2,288 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.4.0": + version "4.4.4" + resolved "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz" + integrity sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@dagrejs/dagre@^1.1.4": - version "1.1.4" - resolved "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.4.tgz" - integrity sha512-QUTc54Cg/wvmlEUxB+uvoPVKFazM1H18kVHBQNmK2NbrDR5ihOCR6CXLnDSZzMcSQKJtabPUWridBOlJM3WkDg== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@dagrejs/graphlib" "2.2.4" + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" -"@dagrejs/graphlib@2.2.4": - version "2.2.4" - resolved "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz" - integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw== +"@babel/compat-data@^7.27.2": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" + integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== + +"@babel/core@^7.0.0", "@babel/core@^7.0.0 || ^8.0.0-0", "@babel/core@^7.0.0-0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.8.0", "@babel/core@>=7.0.0-beta.0 <8": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" -"@emnapi/core@^1.4.3": - version "1.4.3" - resolved "https://registry.npmmirror.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6" - integrity sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g== +"@babel/generator@^7.28.5", "@babel/generator@^7.7.2": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== + dependencies: + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== dependencies: - "@emnapi/wasi-threads" "1.0.2" - tslib "^2.4.0" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" -"@emnapi/runtime@^1.4.0", "@emnapi/runtime@^1.4.3": - version "1.4.3" - resolved "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.4.3.tgz#c0564665c80dc81c448adac23f9dfbed6c838f7d" - integrity sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ== +"@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== dependencies: - tslib "^2.4.0" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" -"@emnapi/wasi-threads@1.0.2": - version "1.0.2" - resolved "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz#977f44f844eac7d6c138a415a123818c655f874c" - integrity sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA== +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== + dependencies: + "@babel/types" "^7.28.5" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.12.5": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + +"@babel/template@^7.27.2", "@babel/template@^7.3.3": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.3.3": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: - tslib "^2.4.0" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0": version "4.7.0" @@ -119,124 +363,30 @@ resolved "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== -"@img/sharp-darwin-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz" - integrity sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A== - optionalDependencies: - "@img/sharp-libvips-darwin-arm64" "1.1.0" - -"@img/sharp-darwin-x64@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz#f1f1d386719f6933796415d84937502b7199a744" - integrity sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q== - optionalDependencies: - "@img/sharp-libvips-darwin-x64" "1.1.0" - -"@img/sharp-libvips-darwin-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz" - integrity sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA== - -"@img/sharp-libvips-darwin-x64@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz#1239c24426c06a8e833815562f78047a3bfbaaf8" - integrity sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ== - -"@img/sharp-libvips-linux-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz#20d276cefd903ee483f0441ba35961679c286315" - integrity sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew== - -"@img/sharp-libvips-linux-arm@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz#067c0b566eae8063738cf1b1db8f8a8573b5465c" - integrity sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA== - -"@img/sharp-libvips-linux-ppc64@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz#682334595f2ca00e0a07a675ba170af165162802" - integrity sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ== - -"@img/sharp-libvips-linux-s390x@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz#82fcd68444b3666384235279c145c2b28d8ee302" - integrity sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA== - "@img/sharp-libvips-linux-x64@1.1.0": version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz#65b2b908bf47156b0724fde9095676c83a18cf5a" + resolved "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz" integrity sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q== -"@img/sharp-libvips-linuxmusl-arm64@1.1.0": - version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz#72accf924e80b081c8db83b900b444a67c203f01" - integrity sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w== - "@img/sharp-libvips-linuxmusl-x64@1.1.0": version "1.1.0" - resolved "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz#1fa052737e203f46bf44192acd01f9faf11522d7" + resolved "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz" integrity sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A== -"@img/sharp-linux-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz#c36ef964499b8cfc2d2ed88fe68f27ce41522c80" - integrity sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ== - optionalDependencies: - "@img/sharp-libvips-linux-arm64" "1.1.0" - -"@img/sharp-linux-arm@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz#c96e38ff028d645912bb0aa132a7178b96997866" - integrity sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA== - optionalDependencies: - "@img/sharp-libvips-linux-arm" "1.1.0" - -"@img/sharp-linux-s390x@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz#8ac58d9a49dcb08215e76c8d450717979b7815c3" - integrity sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA== - optionalDependencies: - "@img/sharp-libvips-linux-s390x" "1.1.0" - "@img/sharp-linux-x64@0.34.1": version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz#3d8652efac635f0dba39d5e3b8b49515a2b2dee1" + resolved "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz" integrity sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA== optionalDependencies: "@img/sharp-libvips-linux-x64" "1.1.0" -"@img/sharp-linuxmusl-arm64@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz#b267e6a3e06f9e4d345cde471e5480c5c39e6969" - integrity sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ== - optionalDependencies: - "@img/sharp-libvips-linuxmusl-arm64" "1.1.0" - "@img/sharp-linuxmusl-x64@0.34.1": version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz#a8dee4b6227f348c4bbacaa6ac3dc584a1a80391" + resolved "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz" integrity sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg== optionalDependencies: "@img/sharp-libvips-linuxmusl-x64" "1.1.0" -"@img/sharp-wasm32@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz#f7dfd66b6c231269042d3d8750c90f28b9ddcba1" - integrity sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg== - dependencies: - "@emnapi/runtime" "^1.4.0" - -"@img/sharp-win32-ia32@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz#4bc293705df76a5f0a02df66ca3dc12e88f61332" - integrity sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw== - -"@img/sharp-win32-x64@0.34.1": - version "0.34.1" - resolved "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz#8a7922fec949f037c204c79f6b83238d2482384b" - integrity sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw== - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz" @@ -249,13 +399,228 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@jridgewell/gen-mapping@^0.3.2": - version "0.3.8" - resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz" - integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.0.0 || ^30.0.0", "@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.0.0 || ^30.0.0", "@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.1.0": @@ -263,33 +628,19 @@ resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": version "1.5.0" resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.24": - version "0.3.25" - resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@napi-rs/wasm-runtime@^0.2.9": - version "0.2.11" - resolved "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz#192c1610e1625048089ab4e35bc0649ce478500e" - integrity sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA== - dependencies: - "@emnapi/core" "^1.4.3" - "@emnapi/runtime" "^1.4.3" - "@tybys/wasm-util" "^0.9.0" - "@next/env@15.3.2": version "15.3.2" resolved "https://registry.npmmirror.com/@next/env/-/env-15.3.2.tgz" @@ -302,46 +653,16 @@ dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.2.tgz" - integrity sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g== - -"@next/swc-darwin-x64@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.2.tgz#3742026344f49128cf1b0f43814c67e880db7361" - integrity sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w== - -"@next/swc-linux-arm64-gnu@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.2.tgz#fb29d45c034e3d2eef89b0e2801d62eb86155823" - integrity sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA== - -"@next/swc-linux-arm64-musl@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.2.tgz#396784ef312666600ab1ae481e34cb1f6e3ae730" - integrity sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg== - "@next/swc-linux-x64-gnu@15.3.2": version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz#ac01fda376878e02bc6b57d1e88ab8ceae9f868e" + resolved "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.2.tgz" integrity sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg== "@next/swc-linux-x64-musl@15.3.2": version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz#327a5023003bcb3ca436efc08733f091bba2b1e8" + resolved "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.2.tgz" integrity sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w== -"@next/swc-win32-arm64-msvc@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.2.tgz#ce3a6588bd9c020960704011ab20bd0440026965" - integrity sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ== - -"@next/swc-win32-x64-msvc@15.3.2": - version "15.3.2" - resolved "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.2.tgz#43cc36097ac27639e9024a5ceaa6e7727fa968c8" - integrity sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA== - "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -350,7 +671,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -442,7 +763,7 @@ "@radix-ui/react-primitive" "2.1.3" "@radix-ui/react-slot" "1.2.3" -"@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1": +"@radix-ui/react-compose-refs@^1.1.1", "@radix-ui/react-compose-refs@1.1.2": version "1.1.2" resolved "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz" integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== @@ -535,7 +856,7 @@ "@radix-ui/react-primitive" "2.1.3" "@radix-ui/react-use-callback-ref" "1.1.1" -"@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0": +"@radix-ui/react-id@^1.1.0", "@radix-ui/react-id@1.1.1": version "1.1.1" resolved "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.1.tgz" integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== @@ -650,7 +971,7 @@ "@radix-ui/react-compose-refs" "1.1.2" "@radix-ui/react-use-layout-effect" "1.1.1" -"@radix-ui/react-primitive@2.1.2", "@radix-ui/react-primitive@^2.0.2": +"@radix-ui/react-primitive@^2.0.2", "@radix-ui/react-primitive@2.1.2": version "2.1.2" resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz" integrity sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw== @@ -713,6 +1034,13 @@ dependencies: "@radix-ui/react-primitive" "2.1.3" +"@radix-ui/react-slot@^1.2.3", "@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-slot@1.2.2": version "1.2.2" resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.2.tgz" @@ -720,13 +1048,6 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.2" -"@radix-ui/react-slot@1.2.3", "@radix-ui/react-slot@^1.2.3": - version "1.2.3" - resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz" - integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== - dependencies: - "@radix-ui/react-compose-refs" "1.1.2" - "@radix-ui/react-switch@^1.2.5": version "1.2.5" resolved "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz" @@ -835,7 +1156,7 @@ resolved "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.1.tgz" integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw== -"@reactflow/background@11.3.14", "@reactflow/background@^11.3.14": +"@reactflow/background@^11.3.14", "@reactflow/background@11.3.14": version "11.3.14" resolved "https://registry.npmmirror.com/@reactflow/background/-/background-11.3.14.tgz" integrity sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA== @@ -881,7 +1202,7 @@ d3-zoom "^3.0.0" zustand "^4.4.1" -"@reactflow/node-resizer@2.2.14", "@reactflow/node-resizer@^2.2.14": +"@reactflow/node-resizer@^2.2.14", "@reactflow/node-resizer@2.2.14": version "2.2.14" resolved "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz" integrity sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA== @@ -911,6 +1232,25 @@ resolved "https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz" integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ== +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@swc/counter@0.1.3": version "0.1.3" resolved "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz" @@ -933,12 +1273,81 @@ lodash.merge "^4.6.2" postcss-selector-parser "6.0.10" -"@tybys/wasm-util@^0.9.0": - version "0.9.0" - resolved "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" - integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== +"@testing-library/dom@^10.0.0": + version "10.4.1" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz" + integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + picocolors "1.1.1" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^6.6.0": + version "6.9.1" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz" + integrity sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + picocolors "^1.1.1" + redent "^3.0.0" + +"@testing-library/react@^16.0.1": + version "16.3.1" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz" + integrity sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw== + dependencies: + "@babel/runtime" "^7.12.5" + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== dependencies: - tslib "^2.4.0" + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.28.0" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" "@types/d3-array@*": version "3.2.1" @@ -1181,6 +1590,13 @@ resolved "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz" integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg== +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + "@types/hast@^3.0.0": version "3.0.4" resolved "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz" @@ -1188,6 +1604,42 @@ dependencies: "@types/unist" "*" +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.14": + version "29.5.14" + resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz" @@ -1205,7 +1657,7 @@ resolved "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@^20": +"@types/node@*", "@types/node@^20": version "20.17.47" resolved "https://registry.npmmirror.com/@types/node/-/node-20.17.47.tgz" integrity sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ== @@ -1217,12 +1669,12 @@ resolved "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.14.tgz" integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== -"@types/react-dom@^18.3.1": +"@types/react-dom@*", "@types/react-dom@^18.0.0 || ^19.0.0", "@types/react-dom@^18.3.1": version "18.3.7" resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz" integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ== -"@types/react@^18.3.1": +"@types/react@*", "@types/react@^18.0.0", "@types/react@^18.0.0 || ^19.0.0", "@types/react@^18.3.1", "@types/react@>=16.8", "@types/react@>=18": version "18.3.21" resolved "https://registry.npmmirror.com/@types/react/-/react-18.3.21.tgz" integrity sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw== @@ -1230,6 +1682,16 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/unist@*", "@types/unist@^3.0.0": version "3.0.3" resolved "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz" @@ -1245,6 +1707,18 @@ resolved "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.35" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.32.1" resolved "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz" @@ -1260,7 +1734,7 @@ natural-compare "^1.4.0" ts-api-utils "^2.1.0" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": +"@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser@^8.0.0 || ^8.0.0-alpha.0": version "8.32.1" resolved "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.32.1.tgz" integrity sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg== @@ -1331,103 +1805,53 @@ resolved "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@unrs/resolver-binding-darwin-arm64@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz" - integrity sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg== - -"@unrs/resolver-binding-darwin-x64@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz#97e0212a85c56e156a272628ec55da7aff992161" - integrity sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ== - -"@unrs/resolver-binding-freebsd-x64@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz#07594a9d1d83e84b52908800459273ea00caf595" - integrity sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg== - -"@unrs/resolver-binding-linux-arm-gnueabihf@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz#9ef6031bb1136ee7862a6f94a2a53c395d2b6fae" - integrity sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw== - -"@unrs/resolver-binding-linux-arm-musleabihf@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz#24910379ab39da1b15d65b1a06b4bfb4c293ca0c" - integrity sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA== - -"@unrs/resolver-binding-linux-arm64-gnu@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz#49b6a8fb8f42f7530f51bc2e60fc582daed31ffb" - integrity sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA== - -"@unrs/resolver-binding-linux-arm64-musl@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz#3a9707a6afda534f30c8de8a5de6c193b1b6d164" - integrity sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA== - -"@unrs/resolver-binding-linux-ppc64-gnu@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz#659831ff2bfe8157d806b69b6efe142265bf9f0f" - integrity sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg== - -"@unrs/resolver-binding-linux-riscv64-gnu@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz#e75abebd53cdddb3d635f6efb7a5ef6e96695717" - integrity sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q== - -"@unrs/resolver-binding-linux-riscv64-musl@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz#e99b5316ee612b180aff5a7211717f3fc8c3e54e" - integrity sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ== - -"@unrs/resolver-binding-linux-s390x-gnu@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz#36646d5f60246f0eae650fc7bcd79b3cbf7dcff1" - integrity sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA== - "@unrs/resolver-binding-linux-x64-gnu@1.7.2": version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz#e720adc2979702c62f4040de05c854f186268c27" + resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz" integrity sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg== "@unrs/resolver-binding-linux-x64-musl@1.7.2": version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz#684e576557d20deb4ac8ea056dcbe79739ca2870" + resolved "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz" integrity sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw== -"@unrs/resolver-binding-wasm32-wasi@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz#5b138ce8d471f5d0c8d6bfab525c53b80ca734e0" - integrity sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g== - dependencies: - "@napi-rs/wasm-runtime" "^0.2.9" - -"@unrs/resolver-binding-win32-arm64-msvc@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz#bd772db4e8a02c31161cf1dfa33852eb7ef22df6" - integrity sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg== - -"@unrs/resolver-binding-win32-ia32-msvc@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz#a6955ccdc43e809a158c4fe2d54931d34c3f7b51" - integrity sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg== +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== -"@unrs/resolver-binding-win32-x64-msvc@1.7.2": - version "1.7.2" - resolved "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz#7fd81d89e34a711d398ca87f6d5842735d49721e" - integrity sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA== +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +acorn-walk@^8.0.2: + version "8.3.4" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.1.0, acorn@^8.11.0, acorn@^8.8.1, acorn@^8.9.0: version "8.14.1" resolved "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== +agent-base@6: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz" @@ -1438,6 +1862,13 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -1455,6 +1886,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz" @@ -1465,7 +1901,7 @@ any-promise@^1.0.0: resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@~3.1.2: +anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -1478,6 +1914,13 @@ arg@^5.0.2: resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz" @@ -1490,11 +1933,18 @@ aria-hidden@^1.2.4: dependencies: tslib "^2.0.0" -aria-query@^5.3.2: +aria-query@^5.0.0, aria-query@^5.3.2: version "5.3.2" resolved "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz" integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== +aria-query@5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz" @@ -1625,6 +2075,69 @@ axobject-query@^4.1.0: resolved "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== +"babel-jest@^29.0.0 || ^30.0.0", babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.2.0" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + bail@^2.0.0: version "2.0.2" resolved "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz" @@ -1635,6 +2148,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.9.0: + version "2.9.11" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz" + integrity sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz" @@ -1667,6 +2185,36 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +browserslist@^4.24.0, "browserslist@>= 4.21.0": + version "4.28.1" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== + dependencies: + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" + +bs-logger@^0.2.6: + version "0.2.6" + resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + busboy@1.6.0: version "1.6.0" resolved "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz" @@ -1710,10 +2258,20 @@ camelcase-css@^2.0.1: resolved "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -caniuse-lite@^1.0.30001579: - version "1.0.30001718" - resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz" - integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw== +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001759: + version "1.0.30001761" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz" + integrity sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g== ccount@^2.0.0: version "2.0.1" @@ -1728,6 +2286,11 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz" @@ -1797,6 +2360,16 @@ chokidar@^3.6.0: optionalDependencies: fsevents "~2.3.2" +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.4.3" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== + class-variance-authority@^0.7.1: version "0.7.1" resolved "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz" @@ -1814,6 +2387,15 @@ client-only@0.0.1: resolved "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clsx@^2.1.1: version "2.1.1" resolved "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz" @@ -1829,6 +2411,16 @@ cmdk@^1.1.1: "@radix-ui/react-id" "^1.1.0" "@radix-ui/react-primitive" "^2.0.2" +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz" + integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz" @@ -1879,7 +2471,25 @@ concat-map@0.0.1: resolved "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -cross-spawn@^7.0.2, cross-spawn@^7.0.6: +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -1904,11 +2514,33 @@ css-what@^6.1.0: resolved "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + csstype@^3.0.2: version "3.1.3" resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz" @@ -1924,7 +2556,7 @@ csstype@^3.0.2: resolved "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz" integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== -"d3-drag@2 - 3", d3-drag@^3.0.0: +d3-drag@^3.0.0, "d3-drag@2 - 3": version "3.0.0" resolved "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz" integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== @@ -1956,7 +2588,7 @@ d3-hierarchy@^1.1.5: dependencies: d3-color "1 - 3" -"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: +d3-selection@^3.0.0, "d3-selection@2 - 3", d3-selection@3: version "3.0.0" resolved "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz" integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== @@ -1993,6 +2625,15 @@ damerau-levenshtein@^1.0.8: resolved "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz" @@ -2027,13 +2668,18 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0: +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@4: version "4.4.1" resolved "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== dependencies: ms "^2.1.3" +decimal.js@^10.4.2: + version "10.6.0" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== + decode-named-character-reference@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz" @@ -2041,11 +2687,21 @@ decode-named-character-reference@^1.0.0: dependencies: character-entities "^2.0.0" +dedent@^1.0.0: + version "1.7.1" + resolved "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz" + integrity sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg== + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz" @@ -2069,7 +2725,7 @@ delayed-stream@~1.0.0: resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -dequal@^2.0.0: +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -2079,6 +2735,11 @@ detect-libc@^2.0.3: resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.4.tgz" integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA== +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz" @@ -2096,6 +2757,11 @@ didyoumean@^1.2.2: resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + dlv@^1.1.3: version "1.1.3" resolved "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz" @@ -2115,6 +2781,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-serializer@^2.0.0: version "2.0.0" resolved "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz" @@ -2129,6 +2805,13 @@ domelementtype@^2.3.0: resolved "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + domhandler@^5.0.2, domhandler@^5.0.3: version "5.0.3" resolved "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz" @@ -2159,9 +2842,19 @@ eastasianwidth@^0.2.0: resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +electron-to-chromium@^1.5.263: + version "1.5.267" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz" + integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: @@ -2177,7 +2870,12 @@ encoding-sniffer@^0.2.0: iconv-lite "^0.6.3" whatwg-encoding "^3.1.1" -entities@^4.2.0, entities@^4.5.0: +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +entities@^4.5.0: version "4.5.0" resolved "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -2187,6 +2885,13 @@ entities@^6.0.0: resolved "https://registry.npmmirror.com/entities/-/entities-6.0.0.tgz" integrity sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw== +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: version "1.23.9" resolved "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.23.9.tgz" @@ -2309,6 +3014,16 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" @@ -2319,6 +3034,17 @@ escape-string-regexp@^5.0.0: resolved "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz" integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== +escodegen@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + eslint-config-next@^15.3.2: version "15.3.2" resolved "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-15.3.2.tgz" @@ -2364,7 +3090,7 @@ eslint-module-utils@^2.12.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.31.0: +eslint-plugin-import@*, eslint-plugin-import@^2.31.0: version "2.31.0" resolved "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz" integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== @@ -2457,7 +3183,7 @@ eslint-visitor-keys@^4.2.0: resolved "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz" integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== -eslint@^8: +eslint@*, "eslint@^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9", "eslint@^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7", "eslint@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", "eslint@^7.23.0 || ^8.0.0 || ^9.0.0", eslint@^8, "eslint@^8.57.0 || ^9.0.0": version "8.57.1" resolved "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz" integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== @@ -2510,6 +3236,11 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + esquery@^1.4.2: version "1.6.0" resolved "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz" @@ -2539,6 +3270,37 @@ esutils@^2.0.2: resolved "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + extend@^3.0.0: version "3.0.2" resolved "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz" @@ -2549,29 +3311,29 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@3.3.1: - version "3.3.1" - resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz" - integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.4" + micromatch "^4.0.8" -fast-glob@^3.3.2: - version "3.3.3" - resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== +fast-glob@3.3.1: + version "3.3.1" + resolved "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.8" + micromatch "^4.0.4" -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0, fast-json-stable-stringify@2.x: version "2.1.0" resolved "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -2588,6 +3350,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + fdir@^6.4.4: version "6.4.4" resolved "https://registry.npmmirror.com/fdir/-/fdir-6.4.4.tgz" @@ -2607,6 +3376,22 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + find-up@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz" @@ -2664,11 +3449,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz" @@ -2691,6 +3471,16 @@ functions-have-names@^1.2.3: resolved "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz" @@ -2712,6 +3502,11 @@ get-nonce@^1.0.0: resolved "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + get-proto@^1.0.0, get-proto@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz" @@ -2720,6 +3515,11 @@ get-proto@^1.0.0, get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + get-symbol-description@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz" @@ -2736,7 +3536,7 @@ get-tsconfig@^4.10.0: dependencies: resolve-pkg-maps "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2750,6 +3550,13 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@^10.3.10: version "10.4.5" resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz" @@ -2762,7 +3569,7 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3: +glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2794,11 +3601,33 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphemer@^1.4.0: version "1.4.0" resolved "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +handlebars@^4.7.8: + version "4.7.8" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.2" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +harmony-reflect@^1.4.6: + version "1.6.2" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== + has-bigints@^1.0.2: version "1.1.0" resolved "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz" @@ -2934,6 +3763,18 @@ hastscript@^9.0.0: property-information "^7.0.0" space-separated-tokens "^2.0.0" +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-url-attributes@^3.0.0: version "3.0.1" resolved "https://registry.npmmirror.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz" @@ -2954,13 +3795,42 @@ htmlparser2@^9.1.0: domutils "^3.1.0" entities "^4.5.0" -iconv-lite@0.6.3, iconv-lite@^0.6.3: +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@^0.6.3, iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" + integrity sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA== + dependencies: + harmony-reflect "^1.4.6" + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz" @@ -2979,11 +3849,24 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz" @@ -3033,6 +3916,11 @@ is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: call-bound "^1.0.3" get-intrinsic "^1.2.6" +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz" @@ -3129,6 +4017,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + is-generator-function@^1.0.10: version "1.1.0" resolved "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.0.tgz" @@ -3179,6 +4072,11 @@ is-plain-obj@^4.0.0: resolved "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-regex@^1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz" @@ -3201,6 +4099,11 @@ is-shared-array-buffer@^1.0.4: dependencies: call-bound "^1.0.3" +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-string@^1.0.7, is-string@^1.1.1: version "1.1.1" resolved "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz" @@ -3255,37 +4158,470 @@ isexe@^2.0.0: resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + iterator.prototype@^1.1.4: version "1.1.5" resolved "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz" integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== dependencies: - define-data-property "^1.1.4" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.6" - get-proto "^1.0.0" - has-symbols "^1.1.0" - set-function-name "^2.0.2" + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-jsdom@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz" + integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jsdom "^20.0.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@*, jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +"jest-util@^29.0.0 || ^30.0.0", jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +"jest@^29.0.0 || ^30.0.0", jest@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" jiti@^1.21.6: version "1.21.7" resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== -"js-tokens@^3.0.0 || ^4.0.0": +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@^3.13.1: + version "3.14.2" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz" @@ -3293,11 +4629,53 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" @@ -3315,6 +4693,11 @@ json5@^1.0.2: dependencies: minimist "^1.2.0" +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: version "3.3.5" resolved "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz" @@ -3332,6 +4715,11 @@ keyv@^4.5.3: dependencies: json-buffer "3.0.1" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz" @@ -3344,6 +4732,11 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + levn@^0.4.1: version "0.4.1" resolved "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz" @@ -3362,6 +4755,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz" @@ -3379,6 +4779,11 @@ lodash.isplainobject@^4.0.6: resolved "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz" integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz" @@ -3401,11 +4806,42 @@ lru-cache@^10.2.0: resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lucide-react@^0.468.0: version "0.468.0" resolved "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.468.0.tgz" integrity sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA== +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@^1.3.6: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + markdown-table@^3.0.0: version "3.0.4" resolved "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz" @@ -3596,16 +5032,16 @@ mdast-util-to-string@^4.0.0: dependencies: "@types/mdast" "^4.0.0" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + merge2@^1.3.0: version "1.4.1" resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz" @@ -3899,7 +5335,17 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -3913,7 +5359,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3930,7 +5376,7 @@ mobx-react-lite@^4.1.0: dependencies: use-sync-external-store "^1.4.0" -mobx@^6.13.5: +mobx@^6.13.5, mobx@^6.9.0: version "6.13.7" resolved "https://registry.npmmirror.com/mobx/-/mobx-6.13.7.tgz" integrity sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g== @@ -3964,9 +5410,14 @@ natural-compare@^1.4.0: resolved "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + next-themes@^0.4.6: version "0.4.6" - resolved "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz#8d7e92d03b8fea6582892a50a928c9b23502e8b6" + resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz" integrity sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA== next@^15.3.2: @@ -3992,11 +5443,28 @@ next@^15.3.2: "@next/swc-win32-x64-msvc" "15.3.2" sharp "^0.34.1" +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + nth-check@^2.0.1: version "2.1.1" resolved "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz" @@ -4004,6 +5472,11 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +nwsapi@^2.2.2: + version "2.2.23" + resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz" + integrity sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ== + object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz" @@ -4082,6 +5555,13 @@ once@^1.3.0: dependencies: wrappy "1" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open-graph-scraper@^6.10.0: version "6.10.0" resolved "https://registry.npmmirror.com/open-graph-scraper/-/open-graph-scraper-6.10.0.tgz" @@ -4113,13 +5593,27 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -p-limit@^3.0.2: +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-locate@^5.0.0: version "5.0.0" resolved "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz" @@ -4127,6 +5621,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" @@ -4152,6 +5651,16 @@ parse-entities@^4.0.0: is-decimal "^2.0.0" is-hexadecimal "^2.0.0" +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parse5-htmlparser2-tree-adapter@^7.0.0: version "7.1.0" resolved "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz" @@ -4167,7 +5676,7 @@ parse5-parser-stream@^7.1.2: dependencies: parse5 "^7.0.0" -parse5@^7.0.0, parse5@^7.1.2: +parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: version "7.3.0" resolved "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz" integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== @@ -4184,7 +5693,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -4202,17 +5711,17 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -picocolors@^1.0.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.1, picocolors@1.1.1: version "1.1.1" resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2: +"picomatch@^3 || ^4", picomatch@^4.0.2: version "4.0.2" resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== @@ -4222,11 +5731,18 @@ pify@^2.3.0: resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== -pirates@^4.0.1: +pirates@^4.0.1, pirates@^4.0.4: version "4.0.7" resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz" integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz" @@ -4263,14 +5779,6 @@ postcss-nested@^6.2.0: dependencies: postcss-selector-parser "^6.1.1" -postcss-selector-parser@6.0.10: - version "6.0.10" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" - integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: version "6.1.2" resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" @@ -4279,11 +5787,28 @@ postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@6.0.10: + version "6.0.10" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-value-parser@^4.0.0: version "4.2.0" resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== +postcss@^8, postcss@^8.0.0, postcss@^8.2.14, postcss@^8.4.21, postcss@^8.4.47, postcss@>=8.0.9: + version "8.5.3" + resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + postcss@8.4.31: version "8.4.31" resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz" @@ -4293,20 +5818,46 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8, postcss@^8.4.47: - version "8.5.3" - resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz" - integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== - dependencies: - nanoid "^3.3.8" - picocolors "^1.1.1" - source-map-js "^1.2.1" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +pretty-format@^29.0.0: + version "29.7.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz" @@ -4331,17 +5882,34 @@ proxy-from-env@^1.1.0: resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -punycode@^2.1.0: +psl@^1.1.33: + version "1.15.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz" + integrity sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w== + dependencies: + punycode "^2.3.1" + +punycode@^2.1.0, punycode@^2.1.1, punycode@^2.3.1: version "2.3.1" resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -react-dom@^18.3.1: +"react-dom@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react-dom@^18.3.1, react-dom@>=16.8.0, react-dom@>=17: version "18.3.1" resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -4354,6 +5922,16 @@ react-is@^16.13.1: resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-markdown@^10.1.0: version "10.1.0" resolved "https://registry.npmmirror.com/react-markdown/-/react-markdown-10.1.0.tgz" @@ -4403,7 +5981,7 @@ react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: get-nonce "^1.0.0" tslib "^2.0.0" -react@^18.3.1: +"react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@>=16.8, react@>=16.8.0, react@>=17, react@>=18: version "18.3.1" resolved "https://registry.npmmirror.com/react/-/react-18.3.1.tgz" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -4436,6 +6014,14 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: version "1.0.10" resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz" @@ -4513,17 +6099,44 @@ remark-stringify@^11.0.0: mdast-util-to-markdown "^2.0.0" unified "^11.0.0" +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@^1.1.7, resolve@^1.22.4, resolve@^1.22.8: +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.4, resolve@^1.22.8: version "1.22.10" resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz" integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== @@ -4593,6 +6206,13 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.23.2: version "0.23.2" resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz" @@ -4600,16 +6220,26 @@ scheduler@^0.23.2: dependencies: loose-envify "^1.1.0" +semver@^6.3.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^6.3.1: version "6.3.1" resolved "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.6.0, semver@^7.7.1: +semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.7.1: version "7.7.2" resolved "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== +semver@^7.7.3: + version "7.7.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz" @@ -4723,6 +6353,16 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz" @@ -4735,31 +6375,74 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + sonner@^2.0.5: - version "2.0.5" - resolved "https://registry.npmmirror.com/sonner/-/sonner-2.0.5.tgz#ffb70a6ffe3207c4302cffd3ee46a25242953477" - integrity sha512-YwbHQO6cSso3HBXlbCkgrgzDNIhws14r4MO87Ofy+cV2X7ES4pOoAK3+veSmVTvqNx1BWUxlhPmZzP00Crk2aQ== + version "2.0.7" + resolved "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz" + integrity sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w== source-map-js@^1.0.2, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + stable-hash@^0.0.5: version "0.0.5" resolved "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz" integrity sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz" @@ -4769,7 +6452,7 @@ streamsearch@^1.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^4.1.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4778,6 +6461,15 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz" @@ -4889,6 +6581,23 @@ strip-bom@^3.0.0: resolved "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz" @@ -4935,11 +6644,23 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tailwind-merge@^2.5.5: version "2.6.0" resolved "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz" @@ -4950,7 +6671,7 @@ tailwindcss-animate@^1.0.7: resolved "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz" integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== -tailwindcss@^3.4.16: +tailwindcss@^3.4.16, "tailwindcss@>=3.0.0 || insiders", "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1": version "3.4.17" resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz" integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og== @@ -4978,6 +6699,15 @@ tailwindcss@^3.4.16: resolve "^1.22.8" sucrase "^3.35.0" +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz" @@ -5005,6 +6735,11 @@ tinyglobby@^0.2.13: fdir "^6.4.4" picomatch "^4.0.2" +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -5012,6 +6747,23 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tough-cookie@^4.1.2: + version "4.1.4" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz" @@ -5032,6 +6784,21 @@ ts-interface-checker@^0.1.9: resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-jest@^29.2.5: + version "29.4.6" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz" + integrity sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA== + dependencies: + bs-logger "^0.2.6" + fast-json-stable-stringify "^2.1.0" + handlebars "^4.7.8" + json5 "^2.2.3" + lodash.memoize "^4.1.2" + make-error "^1.3.6" + semver "^7.7.3" + type-fest "^4.41.0" + yargs-parser "^21.1.1" + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz" @@ -5054,11 +6821,26 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^4.41.0: + version "4.41.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + typed-array-buffer@^1.0.3: version "1.0.3" resolved "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz" @@ -5104,11 +6886,16 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.0.0" reflect.getprototypeof "^1.0.6" -typescript@^5: +typescript@^5, typescript@>=3.3.1, "typescript@>=4.3 <6", typescript@>=4.8.4, "typescript@>=4.8.4 <5.9.0": version "5.8.3" resolved "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz" integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== +uglify-js@^3.1.4: + version "3.19.3" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz" + integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== + unbox-primitive@^1.1.0: version "1.1.0" resolved "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz" @@ -5180,6 +6967,11 @@ unist-util-visit@^5.0.0: unist-util-is "^6.0.0" unist-util-visit-parents "^6.0.0" +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + unrs-resolver@^1.6.2: version "1.7.2" resolved "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.7.2.tgz" @@ -5205,6 +6997,14 @@ unrs-resolver@^1.6.2: "@unrs/resolver-binding-win32-ia32-msvc" "1.7.2" "@unrs/resolver-binding-win32-x64-msvc" "1.7.2" +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz" @@ -5212,6 +7012,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + use-callback-ref@^1.3.3: version "1.3.3" resolved "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz" @@ -5242,6 +7050,15 @@ uuid@^11.1.0: resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz" integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + vfile-location@^5.0.0: version "5.0.3" resolved "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz" @@ -5266,6 +7083,20 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + watch@^0.13.0: version "0.13.0" resolved "https://registry.npmjs.org/watch/-/watch-0.13.0.tgz" @@ -5278,6 +7109,18 @@ web-namespaces@^2.0.0: resolved "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + whatwg-encoding@^3.1.1: version "3.1.1" resolved "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz" @@ -5285,11 +7128,24 @@ whatwg-encoding@^3.1.1: dependencies: iconv-lite "0.6.3" +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + whatwg-mimetype@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz" integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz" @@ -5355,6 +7211,11 @@ word-wrap@^1.2.5: resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz" @@ -5364,6 +7225,15 @@ word-wrap@^1.2.5: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz" @@ -5378,11 +7248,62 @@ wrappy@1: resolved "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^8.11.0: + version "8.18.3" + resolved "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yaml@^2.3.4: version "2.8.0" resolved "https://registry.npmmirror.com/yaml/-/yaml-2.8.0.tgz" integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz" diff --git a/scripts/analyze_session.py b/scripts/analyze_session.py new file mode 100755 index 0000000..1939aad --- /dev/null +++ b/scripts/analyze_session.py @@ -0,0 +1,922 @@ +#!/usr/bin/env python3 +""" +CommonGround Session Analyzer + +A comprehensive tool for deep analysis of CommonGround project sessions. +Provides multi-level observability into: +- Session flow and timeline +- Agent handoffs and delegation +- Token utilization and budget compliance +- Tool usage patterns +- Error detection and diagnosis + +Usage: + python analyze_session.py [--level LEVEL] [--focus FOCUS] + +Input: + Can be either: + - A file path: projects/MyProject/session-id.json + - A session URL: http://localhost:3800/webview/r?id=tentacled-pearl-oriole + - Just a session ID: tentacled-pearl-oriole + +Levels: + summary - High-level overview (default) + detailed - Per-agent breakdown with key metrics + deep - Full message-level analysis + timeline - Chronological event trace + +Focus: + all - Full session analysis (default) + principal - Focus on Principal agent + partner - Focus on Partner agent + WM_N - Focus on specific work module (e.g., WM_1, WM_2) + errors - Focus on errors and issues + tokens - Focus on token utilization + +Examples: + python analyze_session.py http://localhost:3800/webview/r?id=tentacled-pearl-oriole + python analyze_session.py tentacled-pearl-oriole + python analyze_session.py projects/MyProject/session-id.json + python analyze_session.py projects/MyProject/session-id.json --level detailed + python analyze_session.py projects/MyProject/session-id.json --level deep --focus WM_1 + python analyze_session.py projects/MyProject/session-id.json --focus tokens +""" + +import argparse +import json +import sys +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple +from collections import Counter, defaultdict +from dataclasses import dataclass, field +from urllib.parse import urlparse, parse_qs +import re +import glob + +# Add core to path for imports +SCRIPT_DIR = Path(__file__).parent.resolve() +CORE_DIR = SCRIPT_DIR.parent / "core" +PROJECTS_DIR = CORE_DIR / "projects" +sys.path.insert(0, str(CORE_DIR)) + + +# ============================================================================= +# INPUT RESOLUTION +# ============================================================================= + +def resolve_session_input(input_str: str) -> Path: + """ + Resolve various input formats to a session JSON file path. + + Accepts: + - Full file path: /path/to/session.json or projects/MyProject/session.json + - URL: http://localhost:3800/webview/r?id=session-id + - Session ID: tentacled-pearl-oriole + + Returns: + Path to the session JSON file + + Raises: + FileNotFoundError if session cannot be found + """ + input_str = input_str.strip() + session_id = None + + # Check if it's a URL + if input_str.startswith("http://") or input_str.startswith("https://"): + parsed = urlparse(input_str) + query_params = parse_qs(parsed.query) + + # Try to get session ID from 'id' parameter + if "id" in query_params: + session_id = query_params["id"][0] + else: + # Try to extract from path (e.g., /r/session-id) + path_parts = parsed.path.strip("/").split("/") + if path_parts: + session_id = path_parts[-1] + + # Check if it's already a file path + elif input_str.endswith(".json"): + path = Path(input_str) + if path.exists(): + return path + # Try relative to core dir + path = CORE_DIR / input_str + if path.exists(): + return path + raise FileNotFoundError(f"Session file not found: {input_str}") + + # Otherwise treat as session ID + else: + session_id = input_str + + # Search for session ID in projects + if session_id: + # Search all project directories for the session + pattern = str(PROJECTS_DIR / "**" / f"{session_id}.json") + matches = glob.glob(pattern, recursive=True) + + if matches: + # Return the first match (most recent if multiple) + return Path(matches[0]) + + # Also try without the .json extension in case it was included + if session_id.endswith(".json"): + session_id = session_id[:-5] + pattern = str(PROJECTS_DIR / "**" / f"{session_id}.json") + matches = glob.glob(pattern, recursive=True) + if matches: + return Path(matches[0]) + + raise FileNotFoundError( + f"Session '{session_id}' not found in projects directory.\n" + f"Searched: {PROJECTS_DIR}/**/{session_id}.json" + ) + + raise ValueError(f"Could not parse session input: {input_str}") + + +# ============================================================================= +# DATA CLASSES +# ============================================================================= + +@dataclass +class TokenMetrics: + """Token usage metrics for an agent context.""" + message_count: int = 0 + estimated_tokens: int = 0 + context_limit: int = 200000 # Default + utilization_percent: float = 0.0 + status: str = "UNKNOWN" + + def calculate(self): + """Calculate utilization and status.""" + if self.context_limit > 0: + self.utilization_percent = (self.estimated_tokens / self.context_limit) * 100 + + if self.utilization_percent < 40: + self.status = "HEALTHY" + elif self.utilization_percent < 55: + self.status = "WARNING" + elif self.utilization_percent < 70: + self.status = "CRITICAL" + else: + self.status = "EXCEEDED" + + +@dataclass +class AgentSummary: + """Summary of an agent's activity.""" + agent_id: str + agent_type: str # principal, partner, associate + model: str = "unknown" + tokens: TokenMetrics = field(default_factory=TokenMetrics) + tool_calls: Counter = field(default_factory=Counter) + errors: List[Dict] = field(default_factory=list) + duration_seconds: float = 0.0 + status: str = "unknown" + + +@dataclass +class WorkModuleSummary: + """Summary of a work module.""" + module_id: str + name: str + description: str + status: str + assigned_agent: str + agent_profile: str = "unknown" # The profile logical name (e.g., Associate_SmartRAG_EN) + tokens: TokenMetrics = field(default_factory=TokenMetrics) + tool_calls: Counter = field(default_factory=Counter) + deliverables_count: int = 0 + message_count: int = 0 + dispatch_status: str = "unknown" + created_at: Optional[str] = None + updated_at: Optional[str] = None + + +@dataclass +class SessionAnalysis: + """Complete session analysis result.""" + session_id: str + run_type: str + status: str + created_at: Optional[str] + + # Agent summaries + partner: Optional[AgentSummary] = None + principal: Optional[AgentSummary] = None + work_modules: Dict[str, WorkModuleSummary] = field(default_factory=dict) + + # Aggregates + total_tokens: int = 0 + total_messages: int = 0 + total_tool_calls: int = 0 + dispatch_count: int = 0 + successful_dispatches: int = 0 + + # Issues detected + issues: List[Dict] = field(default_factory=list) + warnings: List[str] = field(default_factory=list) + + +# ============================================================================= +# ANALYSIS FUNCTIONS +# ============================================================================= + +def estimate_tokens(content: Any) -> int: + """Estimate token count from content (rough: 1 token β‰ˆ 4 chars).""" + if content is None: + return 0 + return len(str(content)) // 4 + + +def get_context_limit(model_name: str) -> int: + """Get context limit for a model. Tries to use guardian if available.""" + try: + from agent_core.framework.context_budget_guardian import get_model_context_limit + return get_model_context_limit(model_name) + except ImportError: + pass + except Exception: + # Guardian import succeeded but function call failed + pass + + # Fallback defaults - use improved matching + model_lower = model_name.lower() + + # Claude models - all have 200K context + if "claude" in model_lower: + return 200000 + + # OpenAI models + if "gpt-4o" in model_lower or "gpt-4-turbo" in model_lower: + return 128000 + if "gpt-4" in model_lower: + return 8192 + if "gpt-3.5" in model_lower: + return 16385 + + # Gemini models + if "gemini" in model_lower: + return 1000000 + + # Default for unknown models + return 200000 + + +def analyze_messages(messages: List[Dict], model_name: str = "unknown") -> Tuple[TokenMetrics, Counter, List[Dict]]: + """Analyze a list of messages for tokens, tool calls, and errors.""" + metrics = TokenMetrics() + metrics.context_limit = get_context_limit(model_name) + tool_calls = Counter() + errors = [] + + metrics.message_count = len(messages) + + for msg in messages: + if not isinstance(msg, dict): + continue + + # Count tokens + content = msg.get("content", "") + metrics.estimated_tokens += estimate_tokens(content) + + # Count tool calls + for tc in msg.get("tool_calls", []): + if isinstance(tc, dict): + name = tc.get("function", {}).get("name", "unknown") + tool_calls[name] += 1 + + # Detect errors + content_str = str(content).lower() + if "error" in content_str or "failed" in content_str: + if msg.get("role") == "tool": + errors.append({ + "type": "tool_error", + "preview": str(content)[:200] + }) + + metrics.calculate() + return metrics, tool_calls, errors + + +# Default model when not stored in session (Claude Sonnet 4 is the typical default) +DEFAULT_MODEL = "claude-sonnet-4-20250514" + + +def analyze_partner(sub_contexts: Dict) -> Optional[AgentSummary]: + """Analyze Partner agent context.""" + partner_ctx = sub_contexts.get("_partner_context_ref", {}) + if not partner_ctx: + return None + + messages = partner_ctx.get("messages", []) + model = partner_ctx.get("model", DEFAULT_MODEL) + + summary = AgentSummary( + agent_id="Partner", + agent_type="partner", + model=model + ) + + summary.tokens, summary.tool_calls, summary.errors = analyze_messages(messages, model) + summary.status = summary.tokens.status + + return summary + + +def analyze_principal(sub_contexts: Dict) -> Optional[AgentSummary]: + """Analyze Principal agent context.""" + principal_ctx = sub_contexts.get("_principal_context_ref", {}) + if not principal_ctx: + return None + + messages = principal_ctx.get("messages", []) + model = principal_ctx.get("model", DEFAULT_MODEL) + + # Check for context_budget status in the context + budget_status = principal_ctx.get("context_budget", {}) + + summary = AgentSummary( + agent_id="Principal", + agent_type="principal", + model=model + ) + + summary.tokens, summary.tool_calls, summary.errors = analyze_messages(messages, model) + + # Use stored budget status if available + if budget_status: + summary.tokens.status = budget_status.get("status", summary.tokens.status) + if budget_status.get("utilization_percent"): + summary.tokens.utilization_percent = budget_status["utilization_percent"] + else: + summary.status = summary.tokens.status + + return summary + + +def analyze_work_modules(team_state: Dict) -> Dict[str, WorkModuleSummary]: + """Analyze all work modules.""" + work_modules = team_state.get("work_modules", {}) + summaries = {} + + for wm_id, wm in work_modules.items(): + if not isinstance(wm, dict): + continue + + summary = WorkModuleSummary( + module_id=wm_id, + name=wm.get("name", "unnamed"), + description=wm.get("description", "")[:100], + status=wm.get("status", "unknown"), + assigned_agent="unknown", + created_at=wm.get("created_at"), + updated_at=wm.get("updated_at") + ) + + # Get assigned agent from history + assignee_history = wm.get("assignee_history", []) + if assignee_history and isinstance(assignee_history[0], dict): + summary.assigned_agent = assignee_history[0].get("agent", "unknown") + + # Analyze context archive + context_archive = wm.get("context_archive", []) + if isinstance(context_archive, list) and context_archive: + archive = context_archive[0] if isinstance(context_archive[0], dict) else {} + messages = archive.get("messages", []) + model = archive.get("model", DEFAULT_MODEL) + + summary.tokens, summary.tool_calls, _ = analyze_messages(messages, model) + summary.message_count = len(messages) + summary.deliverables_count = len(archive.get("deliverables", [])) + + summaries[wm_id] = summary + + # Analyze dispatch history - get dispatch status and profile name + dispatch_history = team_state.get("dispatch_history", []) + for dispatch in dispatch_history: + if isinstance(dispatch, dict): + module_id = dispatch.get("module_id") + if module_id and module_id in summaries: + summaries[module_id].dispatch_status = dispatch.get("status", "unknown") + # Get the actual profile name from dispatch history + profile_name = dispatch.get("profile_logical_name", "") + if profile_name: + summaries[module_id].agent_profile = profile_name + + return summaries + + +def detect_issues(analysis: SessionAnalysis) -> List[Dict]: + """Detect potential issues in the session.""" + issues = [] + + # Check Principal exceeded budget + if analysis.principal and analysis.principal.tokens.status == "EXCEEDED": + issues.append({ + "severity": "HIGH", + "type": "context_exceeded", + "agent": "Principal", + "details": f"Principal exceeded context budget: {analysis.principal.tokens.utilization_percent:.1f}%" + }) + + # Check for infinite loop patterns (many repeated tool calls) + if analysis.principal: + for tool, count in analysis.principal.tool_calls.most_common(3): + if count > 50 and tool in ["generate_message_summary", "finish_flow"]: + issues.append({ + "severity": "HIGH", + "type": "potential_loop", + "agent": "Principal", + "details": f"Tool '{tool}' called {count} times - possible infinite loop" + }) + + # Check for failed dispatches + for wm_id, wm in analysis.work_modules.items(): + if "FAIL" in wm.dispatch_status.upper(): + issues.append({ + "severity": "MEDIUM", + "type": "dispatch_failed", + "agent": wm_id, + "details": f"Work module {wm_id} dispatch failed: {wm.dispatch_status}" + }) + + # Check for empty deliverables on completed modules + for wm_id, wm in analysis.work_modules.items(): + if wm.status == "pending_review" and wm.deliverables_count == 0: + issues.append({ + "severity": "LOW", + "type": "no_deliverables", + "agent": wm_id, + "details": f"Work module {wm_id} completed but has no deliverables" + }) + + return issues + + +def analyze_session(session_path: Path) -> SessionAnalysis: + """Perform complete session analysis.""" + with open(session_path, 'r') as f: + data = json.load(f) + + meta = data.get("meta", {}) + team_state = data.get("team_state", {}) + sub_contexts = data.get("sub_contexts_state", {}) + + analysis = SessionAnalysis( + session_id=meta.get("run_id", "unknown"), + run_type=meta.get("run_type", "unknown"), + status=meta.get("status", "unknown"), + created_at=meta.get("creation_timestamp") + ) + + # Analyze agents + analysis.partner = analyze_partner(sub_contexts) + analysis.principal = analyze_principal(sub_contexts) + analysis.work_modules = analyze_work_modules(team_state) + + # Calculate aggregates + if analysis.partner: + analysis.total_tokens += analysis.partner.tokens.estimated_tokens + analysis.total_messages += analysis.partner.tokens.message_count + analysis.total_tool_calls += sum(analysis.partner.tool_calls.values()) + + if analysis.principal: + analysis.total_tokens += analysis.principal.tokens.estimated_tokens + analysis.total_messages += analysis.principal.tokens.message_count + analysis.total_tool_calls += sum(analysis.principal.tool_calls.values()) + + for wm in analysis.work_modules.values(): + analysis.total_tokens += wm.tokens.estimated_tokens + analysis.total_messages += wm.message_count + analysis.total_tool_calls += sum(wm.tool_calls.values()) + analysis.dispatch_count += 1 + if "SUCCESS" in wm.dispatch_status.upper(): + analysis.successful_dispatches += 1 + + # Detect issues + analysis.issues = detect_issues(analysis) + + return analysis + + +# ============================================================================= +# OUTPUT FORMATTERS +# ============================================================================= + +class Colors: + """ANSI color codes for terminal output.""" + RESET = "\033[0m" + BOLD = "\033[1m" + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + GRAY = "\033[90m" + + +def status_color(status: str) -> str: + """Get color for a status.""" + status_upper = status.upper() + if status_upper in ["HEALTHY", "SUCCESS", "COMPLETED_SUCCESS"]: + return Colors.GREEN + elif status_upper in ["WARNING", "PENDING_REVIEW"]: + return Colors.YELLOW + elif status_upper in ["CRITICAL"]: + return Colors.MAGENTA + elif status_upper in ["EXCEEDED", "FAILED", "ERROR"]: + return Colors.RED + return Colors.RESET + + +def format_tokens(metrics: TokenMetrics) -> str: + """Format token metrics with color.""" + color = status_color(metrics.status) + return f"{color}{metrics.estimated_tokens:,}{Colors.RESET} / {metrics.context_limit:,} ({metrics.utilization_percent:.1f}% {metrics.status})" + + +def print_header(text: str, char: str = "=", width: int = 80): + """Print a formatted header.""" + print(f"\n{Colors.BOLD}{char * width}") + print(f"{text.center(width)}") + print(f"{char * width}{Colors.RESET}") + + +def print_subheader(text: str, char: str = "-", width: int = 60): + """Print a formatted subheader.""" + print(f"\n{Colors.CYAN}{char * width}") + print(f" {text}") + print(f"{char * width}{Colors.RESET}") + + +def print_summary(analysis: SessionAnalysis): + """Print summary level output.""" + print_header(f"SESSION ANALYSIS: {analysis.session_id}") + + print(f"\n{Colors.BOLD}Session Info:{Colors.RESET}") + print(f" Run Type: {analysis.run_type}") + print(f" Status: {status_color(analysis.status)}{analysis.status}{Colors.RESET}") + print(f" Created: {analysis.created_at or 'unknown'}") + + print(f"\n{Colors.BOLD}Aggregates:{Colors.RESET}") + print(f" Total Messages: {analysis.total_messages:,}") + print(f" Total Tokens: ~{analysis.total_tokens:,}") + print(f" Total Tool Calls: {analysis.total_tool_calls:,}") + print(f" Dispatches: {analysis.successful_dispatches}/{analysis.dispatch_count} successful") + + # Agent overview + print_subheader("AGENT OVERVIEW") + + if analysis.partner: + p = analysis.partner + print(f"\n {Colors.BOLD}Partner{Colors.RESET}") + print(f" Tokens: {format_tokens(p.tokens)}") + print(f" Messages: {p.tokens.message_count}") + + if analysis.principal: + p = analysis.principal + color = status_color(p.tokens.status) + print(f"\n {Colors.BOLD}Principal{Colors.RESET}") + print(f" Tokens: {format_tokens(p.tokens)}") + print(f" Messages: {p.tokens.message_count}") + print(f" Top Tools: {', '.join(f'{t}({c})' for t, c in p.tool_calls.most_common(5))}") + + if analysis.work_modules: + print(f"\n {Colors.BOLD}Work Modules ({len(analysis.work_modules)}){Colors.RESET}") + for wm_id, wm in sorted(analysis.work_modules.items()): + status_c = status_color(wm.dispatch_status) + profile_str = f" ({wm.agent_profile})" if wm.agent_profile != "unknown" else "" + print(f" {wm_id}{profile_str}: {status_c}{wm.dispatch_status}{Colors.RESET} - {wm.tokens.estimated_tokens:,} tokens, {wm.message_count} msgs") + + # Issues + if analysis.issues: + print_subheader(f"ISSUES DETECTED ({len(analysis.issues)})") + for issue in analysis.issues: + sev_color = Colors.RED if issue["severity"] == "HIGH" else (Colors.YELLOW if issue["severity"] == "MEDIUM" else Colors.GRAY) + print(f"\n {sev_color}[{issue['severity']}]{Colors.RESET} {issue['type']}") + print(f" Agent: {issue['agent']}") + print(f" {issue['details']}") + else: + print(f"\n{Colors.GREEN}βœ“ No issues detected{Colors.RESET}") + + +def print_detailed(analysis: SessionAnalysis): + """Print detailed level output.""" + print_summary(analysis) + + # Detailed agent analysis + if analysis.principal: + print_subheader("PRINCIPAL AGENT DETAILS") + p = analysis.principal + print(f"\n Model: {p.model}") + print(f" Token Budget Status: {status_color(p.tokens.status)}{p.tokens.status}{Colors.RESET}") + print(f"\n {Colors.BOLD}Tool Usage:{Colors.RESET}") + for tool, count in p.tool_calls.most_common(): + bar = "β–ˆ" * min(count // 5, 40) + print(f" {tool:40} {count:5} {Colors.GRAY}{bar}{Colors.RESET}") + + if p.errors: + print(f"\n {Colors.RED}Errors ({len(p.errors)}):{Colors.RESET}") + for err in p.errors[:5]: + print(f" - {err['type']}: {err['preview'][:100]}...") + + # Work module details + if analysis.work_modules: + print_subheader("WORK MODULE DETAILS") + for wm_id, wm in sorted(analysis.work_modules.items()): + print(f"\n {Colors.BOLD}{wm_id}: {wm.name[:50]}{Colors.RESET}") + print(f" Profile: {Colors.CYAN}{wm.agent_profile}{Colors.RESET}") + print(f" Status: {status_color(wm.status)}{wm.status}{Colors.RESET}") + print(f" Dispatch: {status_color(wm.dispatch_status)}{wm.dispatch_status}{Colors.RESET}") + print(f" Tokens: {format_tokens(wm.tokens)}") + print(f" Messages: {wm.message_count}") + print(f" Deliverables: {wm.deliverables_count}") + if wm.tool_calls: + tools = ", ".join(f"{t}({c})" for t, c in wm.tool_calls.most_common(5)) + print(f" Tools: {tools}") + + +def print_deep(analysis: SessionAnalysis, focus: str = "all", session_path: Path = None): + """Print deep level output with message-level details.""" + print_detailed(analysis) + + if not session_path: + print(f"\n{Colors.YELLOW}Note: Deep analysis requires session path for message details{Colors.RESET}") + return + + with open(session_path, 'r') as f: + data = json.load(f) + + sub_contexts = data.get("sub_contexts_state", {}) + + # Deep dive based on focus + if focus in ["all", "principal"] and analysis.principal: + print_subheader("PRINCIPAL MESSAGE TRACE") + principal_ctx = sub_contexts.get("_principal_context_ref", {}) + messages = principal_ctx.get("messages", []) + + # Show first 10 and last 10 messages + print(f"\n First 10 messages:") + for i, msg in enumerate(messages[:10]): + role = msg.get("role", "?") + tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] + tc_str = f" -> {tc}" if tc else "" + content_preview = str(msg.get("content", ""))[:80].replace("\n", " ") + print(f" [{i:4}] {role:10}{tc_str}") + + if len(messages) > 20: + print(f"\n ... {len(messages) - 20} messages omitted ...") + + print(f"\n Last 10 messages:") + for i, msg in enumerate(messages[-10:]): + idx = len(messages) - 10 + i + role = msg.get("role", "?") + tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] + tc_str = f" -> {tc}" if tc else "" + print(f" [{idx:4}] {role:10}{tc_str}") + + # Work module deep dive + if focus.startswith("WM_"): + team_state = data.get("team_state", {}) + work_modules = team_state.get("work_modules", {}) + wm = work_modules.get(focus) + if wm: + print_subheader(f"DEEP DIVE: {focus}") + context_archive = wm.get("context_archive", []) + if context_archive and isinstance(context_archive[0], dict): + archive = context_archive[0] + messages = archive.get("messages", []) + + print(f"\n All {len(messages)} messages:") + for i, msg in enumerate(messages): + role = msg.get("role", "?") + tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] + tc_str = f" -> {tc}" if tc else "" + content = str(msg.get("content", ""))[:100].replace("\n", " ") + print(f" [{i:3}] {role:10}{tc_str}") + if content and role == "assistant" and not tc: + print(f" {Colors.GRAY}{content}...{Colors.RESET}") + + +def print_token_focus(analysis: SessionAnalysis): + """Print token-focused analysis.""" + print_header("TOKEN UTILIZATION ANALYSIS") + + print(f"\n{Colors.BOLD}Budget Thresholds:{Colors.RESET}") + print(f" {Colors.GREEN}HEALTHY{Colors.RESET}: < 40%") + print(f" {Colors.YELLOW}WARNING{Colors.RESET}: 40-55%") + print(f" {Colors.MAGENTA}CRITICAL{Colors.RESET}: 55-70%") + print(f" {Colors.RED}EXCEEDED{Colors.RESET}: > 70%") + + print_subheader("TOKEN UTILIZATION BY AGENT") + + # Create a visual chart + agents = [] + if analysis.partner: + agents.append(("Partner", analysis.partner.tokens)) + if analysis.principal: + agents.append(("Principal", analysis.principal.tokens)) + for wm_id, wm in sorted(analysis.work_modules.items()): + agents.append((wm_id, wm.tokens)) + + # Find max for scaling + max_tokens = max(a[1].estimated_tokens for a in agents) if agents else 1 + + for name, metrics in agents: + bar_width = int((metrics.estimated_tokens / max_tokens) * 40) if max_tokens > 0 else 0 + color = status_color(metrics.status) + bar = "β–ˆ" * bar_width + + print(f"\n {name:20}") + print(f" {color}{bar}{Colors.RESET}") + print(f" {metrics.estimated_tokens:,} / {metrics.context_limit:,} tokens ({metrics.utilization_percent:.1f}%)") + print(f" Status: {color}{metrics.status}{Colors.RESET}") + + # Summary + total_available = sum(a[1].context_limit for a in agents) + total_used = sum(a[1].estimated_tokens for a in agents) + overall_util = (total_used / total_available * 100) if total_available > 0 else 0 + + print_subheader("SUMMARY") + print(f"\n Total tokens used: {total_used:,}") + print(f" Total available: {total_available:,}") + print(f" Overall utilization: {overall_util:.1f}%") + + +def print_timeline(analysis: SessionAnalysis, session_path: Path = None): + """Print chronological event timeline.""" + print_header("SESSION TIMELINE") + + if not session_path: + print(f"\n{Colors.YELLOW}Note: Timeline requires session path{Colors.RESET}") + return + + with open(session_path, 'r') as f: + data = json.load(f) + + team_state = data.get("team_state", {}) + work_modules = team_state.get("work_modules", {}) + dispatch_history = team_state.get("dispatch_history", []) + + # Collect timeline events + events = [] + + # Session start + meta = data.get("meta", {}) + if meta.get("creation_timestamp"): + events.append({ + "time": meta["creation_timestamp"], + "type": "session_start", + "details": f"Session created: {analysis.run_type}" + }) + + # Work module creation and updates + for wm_id, wm in work_modules.items(): + if wm.get("created_at"): + events.append({ + "time": wm["created_at"], + "type": "wm_created", + "details": f"{wm_id} created: {wm.get('name', 'unnamed')[:40]}" + }) + if wm.get("updated_at"): + events.append({ + "time": wm["updated_at"], + "type": "wm_updated", + "details": f"{wm_id} updated: status={wm.get('status', 'unknown')}" + }) + + # Sort by time + events.sort(key=lambda e: e.get("time", "")) + + print(f"\n{Colors.BOLD}Chronological Events:{Colors.RESET}") + for i, event in enumerate(events): + type_colors = { + "session_start": Colors.BLUE, + "wm_created": Colors.CYAN, + "wm_updated": Colors.GREEN, + "dispatch": Colors.YELLOW, + "error": Colors.RED + } + color = type_colors.get(event["type"], Colors.RESET) + time_str = event.get("time", "unknown")[:19] # Trim to readable format + + print(f"\n {Colors.GRAY}{time_str}{Colors.RESET}") + print(f" {color}[{event['type']:15}]{Colors.RESET} {event['details']}") + + # Dispatch summary + if dispatch_history: + print_subheader("DISPATCH SEQUENCE") + for i, dispatch in enumerate(dispatch_history): + status = dispatch.get("status", "unknown") + color = status_color(status) + print(f" {i+1}. {dispatch.get('module_id', '?'):10} {color}{status}{Colors.RESET}") + + +# ============================================================================= +# MAIN +# ============================================================================= + +def main(): + parser = argparse.ArgumentParser( + description="Analyze CommonGround session for debugging and observability", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__ + ) + parser.add_argument("session_input", + help="Session file path, URL (http://localhost:3800/webview/r?id=SESSION_ID), or session ID") + parser.add_argument("--level", "-l", + choices=["summary", "detailed", "deep", "timeline"], + default="summary", + help="Analysis detail level (default: summary)") + parser.add_argument("--focus", "-f", + default="all", + help="Focus area: all, principal, partner, WM_N, errors, tokens") + parser.add_argument("--no-color", action="store_true", + help="Disable colored output") + parser.add_argument("--json", action="store_true", + help="Output as JSON instead of formatted text") + + args = parser.parse_args() + + # Disable colors if requested + if args.no_color: + for attr in dir(Colors): + if not attr.startswith("_"): + setattr(Colors, attr, "") + + # Resolve session input to file path + try: + session_path = resolve_session_input(args.session_input) + print(f"{Colors.GRAY}Resolved session: {session_path}{Colors.RESET}\n") + except (FileNotFoundError, ValueError) as e: + print(f"{Colors.RED}Error: {e}{Colors.RESET}") + sys.exit(1) + + # Perform analysis + try: + analysis = analyze_session(session_path) + except json.JSONDecodeError as e: + print(f"{Colors.RED}Error: Invalid JSON in session file: {e}{Colors.RESET}") + sys.exit(1) + except Exception as e: + print(f"{Colors.RED}Error analyzing session: {e}{Colors.RESET}") + raise + + # Output based on format + if args.json: + # Convert to JSON-serializable dict + output = { + "session_id": analysis.session_id, + "run_type": analysis.run_type, + "status": analysis.status, + "created_at": analysis.created_at, + "total_tokens": analysis.total_tokens, + "total_messages": analysis.total_messages, + "total_tool_calls": analysis.total_tool_calls, + "dispatch_count": analysis.dispatch_count, + "successful_dispatches": analysis.successful_dispatches, + "issues": analysis.issues, + "partner": { + "tokens": analysis.partner.tokens.estimated_tokens if analysis.partner else 0, + "status": analysis.partner.tokens.status if analysis.partner else "N/A", + } if analysis.partner else None, + "principal": { + "tokens": analysis.principal.tokens.estimated_tokens if analysis.principal else 0, + "status": analysis.principal.tokens.status if analysis.principal else "N/A", + "utilization_percent": analysis.principal.tokens.utilization_percent if analysis.principal else 0, + } if analysis.principal else None, + "work_modules": { + wm_id: { + "agent_profile": wm.agent_profile, + "status": wm.status, + "dispatch_status": wm.dispatch_status, + "tokens": wm.tokens.estimated_tokens, + "messages": wm.message_count, + } + for wm_id, wm in analysis.work_modules.items() + } + } + print(json.dumps(output, indent=2)) + return + + # Formatted output based on level and focus + if args.focus == "tokens": + print_token_focus(analysis) + elif args.level == "timeline": + print_timeline(analysis, session_path) + elif args.level == "deep": + print_deep(analysis, args.focus, session_path) + elif args.level == "detailed": + print_detailed(analysis) + else: + print_summary(analysis) + + +if __name__ == "__main__": + main() diff --git a/scripts/commonground.sh b/scripts/commonground.sh new file mode 100755 index 0000000..d99bd87 --- /dev/null +++ b/scripts/commonground.sh @@ -0,0 +1,417 @@ +#!/bin/bash +# CommonGround Service Manager +# Usage: commonground.sh [start|stop|status|restart] [backend|frontend|all] +# +# Examples: +# commonground.sh start # Start both backend and frontend +# commonground.sh start backend # Start backend only +# commonground.sh stop frontend # Stop frontend only +# commonground.sh status # Show status of all services +# commonground.sh restart # Restart both services + +set -e + +# Configuration +PROJECT_DIR="$HOME/workspaces/git/CommonGround" +VENV_DIR="$HOME/workspaces/venvs/CommonGround" +PID_DIR="$PROJECT_DIR/.pids" +LOG_DIR="$PROJECT_DIR/logs" + +BACKEND_PORT=8800 +FRONTEND_PORT=3800 + +# Create directories +mkdir -p "$PID_DIR" "$LOG_DIR" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +####################################### +# Utility functions +####################################### + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +is_running() { + local pid_file="$1" + if [ -f "$pid_file" ]; then + local pid=$(cat "$pid_file") + if kill -0 "$pid" 2>/dev/null; then + return 0 + fi + fi + return 1 +} + +get_pid() { + local pid_file="$1" + if [ -f "$pid_file" ]; then + cat "$pid_file" + fi +} + +####################################### +# Backend functions +####################################### + +start_backend() { + local pid_file="$PID_DIR/backend.pid" + + if is_running "$pid_file"; then + log_info "Backend is already running (PID: $(get_pid "$pid_file"))" + return 0 + fi + + # Check venv exists + if [ ! -d "$VENV_DIR" ]; then + log_error "Virtual environment not found at $VENV_DIR" + log_error "Run: uv venv $VENV_DIR --python 3.12 && source $VENV_DIR/bin/activate && cd $PROJECT_DIR/core && uv pip install -r requirements.txt" + return 1 + fi + + log_info "Starting backend on port $BACKEND_PORT..." + + # Start backend in background + ( + source "$VENV_DIR/bin/activate" + cd "$PROJECT_DIR/core" + exec python3 run_server.py --host 0.0.0.0 --port $BACKEND_PORT + ) > "$LOG_DIR/backend.log" 2>&1 & + + local pid=$! + echo "$pid" > "$pid_file" + + # Wait a moment and verify it started + sleep 2 + if is_running "$pid_file"; then + log_info "Backend started (PID: $pid)" + log_info "Access: http://localhost:$BACKEND_PORT" + log_info "Log: $LOG_DIR/backend.log" + else + log_error "Backend failed to start. Check $LOG_DIR/backend.log" + rm -f "$pid_file" + return 1 + fi +} + +stop_backend() { + local pid_file="$PID_DIR/backend.pid" + + if ! is_running "$pid_file"; then + log_info "Backend is not running" + rm -f "$pid_file" + return 0 + fi + + local pid=$(get_pid "$pid_file") + log_info "Stopping backend (PID: $pid)..." + + # Send SIGTERM + kill "$pid" 2>/dev/null + + # Wait for graceful shutdown (up to 10 seconds) + local count=0 + while [ $count -lt 10 ]; do + if ! kill -0 "$pid" 2>/dev/null; then + break + fi + sleep 1 + ((count++)) || true + done + + # Force kill if still running + if kill -0 "$pid" 2>/dev/null; then + log_warn "Force killing backend..." + kill -9 "$pid" 2>/dev/null || true + sleep 1 + fi + + rm -f "$pid_file" + log_info "Backend stopped" +} + +status_backend() { + local pid_file="$PID_DIR/backend.pid" + + if is_running "$pid_file"; then + echo -e "Backend: ${GREEN}RUNNING${NC} (PID: $(get_pid "$pid_file"), Port: $BACKEND_PORT)" + else + echo -e "Backend: ${RED}STOPPED${NC}" + rm -f "$pid_file" 2>/dev/null + fi +} + +####################################### +# Frontend functions +####################################### + +start_frontend() { + local pid_file="$PID_DIR/frontend.pid" + + if is_running "$pid_file"; then + log_info "Frontend is already running (PID: $(get_pid "$pid_file"))" + return 0 + fi + + # Check for orphaned process on port (use fuser, more reliable than lsof) + local orphan_pid=$(fuser $FRONTEND_PORT/tcp 2>/dev/null | tr -d ' ' || true) + if [ -n "$orphan_pid" ]; then + log_warn "Found orphaned process on port $FRONTEND_PORT (PID: $orphan_pid), killing..." + kill -9 $orphan_pid 2>/dev/null || true + sleep 1 + fi + + # Check npm exists, offer to install if not + if ! command -v npm &> /dev/null; then + log_warn "npm not found. Attempting to install Node.js..." + + # Try to install Node.js + if command -v curl &> /dev/null; then + log_info "Installing Node.js 20.x via NodeSource..." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && \ + sudo apt-get install -y nodejs + + if ! command -v npm &> /dev/null; then + log_error "Node.js installation failed" + return 1 + fi + log_info "Node.js installed successfully" + else + log_error "curl not found. Cannot auto-install Node.js" + log_error "Install manually: sudo apt-get install -y nodejs npm" + return 1 + fi + fi + + # Check if dependencies installed + if [ ! -d "$PROJECT_DIR/frontend/node_modules" ]; then + log_info "Installing frontend dependencies..." + (cd "$PROJECT_DIR/frontend" && npm install) + fi + + log_info "Starting frontend on port $FRONTEND_PORT..." + + # Start frontend in background + ( + cd "$PROJECT_DIR/frontend" + exec npm run dev -- -H 0.0.0.0 -p $FRONTEND_PORT + ) > "$LOG_DIR/frontend.log" 2>&1 & + + local pid=$! + echo "$pid" > "$pid_file" + + # Wait a moment and verify it started + sleep 3 + if is_running "$pid_file"; then + log_info "Frontend started (PID: $pid)" + log_info "Access: http://localhost:$FRONTEND_PORT" + log_info "Log: $LOG_DIR/frontend.log" + else + log_error "Frontend failed to start. Check $LOG_DIR/frontend.log" + rm -f "$pid_file" + return 1 + fi +} + +stop_frontend() { + local pid_file="$PID_DIR/frontend.pid" + + # Helper function to get PIDs on frontend port + get_port_pids() { + fuser $FRONTEND_PORT/tcp 2>/dev/null | tr -d ' ' || true + } + + if ! is_running "$pid_file"; then + log_info "Frontend is not running" + rm -f "$pid_file" + # Also check if port is in use by orphaned process + local orphan_pid=$(get_port_pids) + if [ -n "$orphan_pid" ]; then + log_warn "Found orphaned process on port $FRONTEND_PORT (PID: $orphan_pid), killing..." + kill -9 $orphan_pid 2>/dev/null || true + fi + return 0 + fi + + local pid=$(get_pid "$pid_file") + log_info "Stopping frontend (PID: $pid)..." + + # Kill all processes using the frontend port (catches npm + next-server children) + local port_pids=$(get_port_pids) + if [ -n "$port_pids" ]; then + kill $port_pids 2>/dev/null || true + fi + + # Also kill the parent process and its children by PID + pkill -P "$pid" 2>/dev/null || true + kill "$pid" 2>/dev/null || true + + # Wait for graceful shutdown + local count=0 + while [ $count -lt 5 ]; do + port_pids=$(get_port_pids) + if [ -z "$port_pids" ]; then + break + fi + sleep 1 + ((count++)) || true + done + + # Force kill if port still in use + port_pids=$(get_port_pids) + if [ -n "$port_pids" ]; then + log_warn "Force killing remaining processes..." + kill -9 $port_pids 2>/dev/null || true + sleep 1 + fi + + rm -f "$pid_file" + log_info "Frontend stopped" +} + +status_frontend() { + local pid_file="$PID_DIR/frontend.pid" + + if is_running "$pid_file"; then + echo -e "Frontend: ${GREEN}RUNNING${NC} (PID: $(get_pid "$pid_file"), Port: $FRONTEND_PORT)" + else + echo -e "Frontend: ${RED}STOPPED${NC}" + rm -f "$pid_file" 2>/dev/null + fi +} + +clean_frontend() { + log_info "Clearing Next.js cache..." + rm -rf "$PROJECT_DIR/frontend/.next" + rm -rf "$PROJECT_DIR/frontend/node_modules/.cache" + log_info "Next.js cache cleared" +} + +####################################### +# Combined functions +####################################### + +start_all() { + start_backend + start_frontend +} + +stop_all() { + stop_frontend + stop_backend +} + +status_all() { + echo "==========================================" + echo " CommonGround Service Status" + echo "==========================================" + status_backend + status_frontend + echo "==========================================" +} + +restart_all() { + stop_all + clean_frontend + sleep 1 + start_all +} + +####################################### +# Main +####################################### + +show_usage() { + echo "Usage: $(basename "$0") [command] [service]" + echo "" + echo "Commands:" + echo " start Start service(s)" + echo " stop Stop service(s)" + echo " restart Restart service(s)" + echo " status Show service status" + echo " logs Tail service logs" + echo " clean Clear caches (Next.js .next folder)" + echo "" + echo "Services:" + echo " backend Backend API server (port $BACKEND_PORT)" + echo " frontend Frontend dev server (port $FRONTEND_PORT)" + echo " all Both services (default)" + echo "" + echo "Examples:" + echo " $(basename "$0") start # Start both" + echo " $(basename "$0") start backend # Start backend only" + echo " $(basename "$0") stop frontend # Stop frontend only" + echo " $(basename "$0") status # Show status" + echo " $(basename "$0") logs backend # Tail backend logs" + echo " $(basename "$0") clean frontend # Clear Next.js cache" +} + +# Parse arguments +COMMAND="${1:-status}" +SERVICE="${2:-all}" + +case "$COMMAND" in + start) + case "$SERVICE" in + backend) start_backend ;; + frontend) start_frontend ;; + all) start_all ;; + *) log_error "Unknown service: $SERVICE"; show_usage; exit 1 ;; + esac + ;; + stop) + case "$SERVICE" in + backend) stop_backend ;; + frontend) stop_frontend ;; + all) stop_all ;; + *) log_error "Unknown service: $SERVICE"; show_usage; exit 1 ;; + esac + ;; + restart) + case "$SERVICE" in + backend) stop_backend; sleep 1; start_backend ;; + frontend) stop_frontend; clean_frontend; sleep 1; start_frontend ;; + all) restart_all ;; + *) log_error "Unknown service: $SERVICE"; show_usage; exit 1 ;; + esac + ;; + status) + status_all + ;; + logs) + case "$SERVICE" in + backend) tail -f "$LOG_DIR/backend.log" ;; + frontend) tail -f "$LOG_DIR/frontend.log" ;; + all) tail -f "$LOG_DIR/backend.log" "$LOG_DIR/frontend.log" ;; + *) log_error "Unknown service: $SERVICE"; show_usage; exit 1 ;; + esac + ;; + clean) + case "$SERVICE" in + frontend|all) clean_frontend ;; + backend) log_info "No cache to clean for backend" ;; + *) log_error "Unknown service: $SERVICE"; show_usage; exit 1 ;; + esac + ;; + -h|--help|help) + show_usage + ;; + *) + log_error "Unknown command: $COMMAND" + show_usage + exit 1 + ;; +esac From a4a6ee76716543a1c2fdcf7fff52493c64b96a18 Mon Sep 17 00:00:00 2001 From: Myles Dear Date: Tue, 30 Dec 2025 23:18:42 -0500 Subject: [PATCH 2/9] feat(context-budget): Add context budget management with agent-type awareness Context Budget System: - Add context_admission_controller for pre-admission budget enforcement - Add context_budget_handback for Principal-delegated summarization - Update thresholds: WARNING 60%, CRITICAL 75%, EXCEEDED 85% - Implement agent-type-aware forcing (Principal/Associate only) - Partner agents receive guidance only (no flow-ending tools) Orphan Detection: - Add detect_orphaned_tool_interactions() to turn_manager - Add finalize_orphaned_tool_interactions() for recovery - Add detect_dispatch_anomalies() to dispatcher_node Session Analysis (analyze_session.py): - Add --mode handoff/thrashing/errors analysis modes - Fix analyze_work_modules() to aggregate ALL context_archive entries - Add dispatch_count tracking for thrashing detection - Improve error detection to avoid false positives Bug Fixes: - Fix DuckDBRAGStore unawaited coroutine warning (lazy init) - Rename test_jina_* to check_jina_* to avoid pytest auto-discovery - Remove unused pythonjsonlogger import (deprecation warning) - Fix sessionManager to always create fresh session_id for WS Frontend: - Increase node fallback dimensions for better visual fit - Fix sessionManager reconnection flow Docs: - Update context-budget-management.md with implementation status Tests: 934 passed, 1 skipped --- core/agent_core/config/logging_config.py | 1 - core/agent_core/events/ingestors.py | 39 +- core/agent_core/flow.py | 82 +- .../framework/context_admission_controller.py | 476 ++++++++++ .../framework/context_budget_guardian.py | 190 +++- .../framework/context_budget_handback.py | 292 +++++++ core/agent_core/framework/turn_manager.py | 82 +- core/agent_core/nodes/base_agent_node.py | 190 ++-- core/agent_core/nodes/base_tool_node.py | 80 +- .../nodes/custom_nodes/dispatcher_node.py | 235 ++++- .../nodes/custom_nodes/jina_search_node.py | 23 + .../nodes/custom_nodes/jina_visit_node.py | 24 + core/agent_core/rag/duckdb_api.py | 18 +- core/agent_core/services/jina_api.py | 16 +- .../llm_configs/associate_llm.yaml | 3 + .../test_context_admission_controller.py | 423 +++++++++ core/tests/test_context_budget_guardian.py | 54 +- core/tests/test_context_budget_handback.py | 361 ++++++++ core/tests/test_jina_api.py | 40 +- .../test_turn_manager_orphan_detection.py | 566 ++++++++++++ .../architecture/context-budget-management.md | 59 +- frontend/app/chat/lib/flow-utils.ts | 14 +- frontend/app/stores/sessionStore.ts | 3 +- frontend/lib/sessionManager.ts | 94 +- scripts/analyze_session.py | 816 +++++++++++++++--- 25 files changed, 3845 insertions(+), 336 deletions(-) create mode 100644 core/agent_core/framework/context_admission_controller.py create mode 100644 core/agent_core/framework/context_budget_handback.py create mode 100644 core/tests/test_context_admission_controller.py create mode 100644 core/tests/test_context_budget_handback.py create mode 100644 core/tests/test_turn_manager_orphan_detection.py diff --git a/core/agent_core/config/logging_config.py b/core/agent_core/config/logging_config.py index 883c4d4..032373b 100644 --- a/core/agent_core/config/logging_config.py +++ b/core/agent_core/config/logging_config.py @@ -2,7 +2,6 @@ import logging import sys import os -from pythonjsonlogger import jsonlogger from contextvars import ContextVar, copy_context import asyncio diff --git a/core/agent_core/events/ingestors.py b/core/agent_core/events/ingestors.py index 323b4de..e538b62 100644 --- a/core/agent_core/events/ingestors.py +++ b/core/agent_core/events/ingestors.py @@ -153,9 +153,41 @@ def work_modules_ingestor(payload: Any, params: Dict, context: Dict) -> str: 'new_messages_from_associate', # Associate work history } + def _get_deliverables_summary(module_data: dict) -> str: + """ + Get deliverables summary, checking BOTH top-level deliverables[] AND context_archive. + + The system stores actual deliverables in context_archive[].deliverables.primary_summary, + but the legacy work_modules[].deliverables[] array often stays empty. This function + checks both locations to give accurate status to the Principal. + """ + # First check top-level deliverables field + top_level = module_data.get('deliverables') + if top_level: + if isinstance(top_level, dict): + return f"({len(top_level)} items)" + elif isinstance(top_level, list) and len(top_level) > 0: + return f"({len(top_level)} items)" + elif top_level: # Some other truthy value + return "(present)" + + # Check context_archive for deliverables (this is where they actually live) + context_archive = module_data.get('context_archive', []) + if context_archive: + for archive in context_archive: + if isinstance(archive, dict): + archive_deliverables = archive.get('deliverables', {}) + if isinstance(archive_deliverables, dict): + primary_summary = archive_deliverables.get('primary_summary', '') + if primary_summary: + # Return char count to indicate deliverables exist + return f"(in archive: {len(primary_summary):,} chars)" + + return "(none)" + # Fields to SUMMARIZE (show counts/metadata only) SUMMARIZE_FIELDS = { - 'deliverables': lambda v: f"({len(v)} items)" if isinstance(v, dict) else "(present)" if v else "(none)", + 'deliverables': None, # Handled specially by _get_deliverables_summary 'tools_used': lambda v: ', '.join(v[:5]) + ('...' if len(v) > 5 else '') if isinstance(v, list) else str(v), } @@ -171,7 +203,10 @@ def _filter_module(module_data: dict) -> dict: "approx_size": len(str(value)) }) continue - if key in SUMMARIZE_FIELDS: + if key == 'deliverables': + # Use special handler that checks both locations + filtered[key] = _get_deliverables_summary(module_data) + elif key in SUMMARIZE_FIELDS and SUMMARIZE_FIELDS[key]: filtered[key] = SUMMARIZE_FIELDS[key](value) elif isinstance(value, dict) and len(str(value)) > 5000: # Recursively filter nested dicts that are too large diff --git a/core/agent_core/flow.py b/core/agent_core/flow.py index e8afb7d..2865088 100644 --- a/core/agent_core/flow.py +++ b/core/agent_core/flow.py @@ -223,20 +223,91 @@ async def run_associate_async(associate_context: dict): final_state["deliverables"] = {} logger.debug("associate_deliverables_initialized", extra={"executing_associate_id": executing_associate_id}) + # =============================================================== + # HANDLE CONTEXT BUDGET HANDBACK + # If agent exceeded context budget, update dispatch history with handback + # =============================================================== + deliverables = final_state.get("deliverables", {}) + if deliverables.get("status") == "CONTEXT_BUDGET_EXCEEDED": + team_state = associate_context['refs']['team'] + handback_data = deliverables.get("_handback", {}) + + # Find and update the dispatch history entry + for entry in reversed(team_state.get("dispatch_history", [])): + if entry.get("dispatch_id") == executing_associate_id: + entry["status"] = "PARTIAL_HANDBACK" + entry["handback"] = handback_data + entry["end_timestamp"] = datetime.now(timezone.utc).isoformat() + entry["termination_reason"] = "context_budget_exceeded" + + logger.info("dispatcher_history_updated_handback", extra={ + "dispatch_id": executing_associate_id, + "new_status": "PARTIAL_HANDBACK", + "kb_tokens_available": handback_data.get("kb_token_count", 0), + "tool_calls_completed": len(handback_data.get("tool_calls_completed", [])) + }) + break + + # Notify Principal of handback if context available + try: + from .framework.context_budget_handback import ( + ContextBudgetHandback, + notify_principal_of_handback + ) + + # Find Principal context through refs + principal_context = associate_context.get('refs', {}).get('run', {}).get('principal_context') + if principal_context and handback_data: + handback = ContextBudgetHandback.from_dict(handback_data) + notify_principal_of_handback(principal_context, handback) + logger.info("principal_notified_of_handback", extra={ + "executing_associate_id": executing_associate_id, + "principal_inbox_items": len(principal_context.get("state", {}).get("inbox", [])) + }) + except Exception as e: + logger.warning("principal_handback_notification_failed", extra={ + "executing_associate_id": executing_associate_id, + "error": str(e) + }) + # =============================================================== + return associate_context except asyncio.CancelledError: # ... (CancelledError handling logic remains unchanged) ... cancel_msg = f"Associate flow (Agent ID: {executing_associate_id}, Run ID: {current_run_id}) was cancelled." logger.info("associate_flow_cancelled", extra={"executing_associate_id": executing_associate_id, "run_id": current_run_id}) + + # Update dispatch history with cancellation info + team_state_from_refs = associate_context['refs']['team'] + if history_entry := next((h for h in reversed(team_state_from_refs.get("dispatch_history", [])) if h.get("dispatch_id") == executing_associate_id), None): + history_entry["_flow_cancelled"] = True + history_entry["_flow_cancelled_at"] = datetime.now(timezone.utc).isoformat() + final_state = associate_context.setdefault("state", {}) final_state["error_message"] = cancel_msg final_state.setdefault("deliverables", {})["error"] = "Flow was cancelled." return associate_context except Exception as e: - # ... (Exception handling logic remains unchanged) ... + # Enhanced exception handling with structured logging error_msg = f"Associate flow error (Agent ID: {executing_associate_id}, Run ID: {current_run_id}): {str(e)}" - logger.error("associate_flow_error", extra={"executing_associate_id": executing_associate_id, "run_id": current_run_id, "error_message": str(e)}, exc_info=True) + logger.error("associate_flow_error", extra={ + "executing_associate_id": executing_associate_id, + "run_id": current_run_id, + "error_message": str(e), + "error_type": type(e).__name__, + "module_id": associate_meta.get("module_id"), + "profile": profile_logical_name_used, + }, exc_info=True) + + # Update dispatch history with detailed error info + team_state_from_refs = associate_context['refs']['team'] + if history_entry := next((h for h in reversed(team_state_from_refs.get("dispatch_history", [])) if h.get("dispatch_id") == executing_associate_id), None): + history_entry["_flow_error"] = True + history_entry["_flow_error_at"] = datetime.now(timezone.utc).isoformat() + history_entry["_flow_error_type"] = type(e).__name__ + history_entry["_flow_error_message"] = str(e)[:500] # Truncate for storage + final_state = associate_context.setdefault("state", {}) final_state["error_message"] = error_msg final_state.setdefault("deliverables", {})["error"] = f"Flow execution failed: {str(e)}" @@ -248,6 +319,13 @@ async def run_associate_async(associate_context: dict): await release_mcp_session_to_pool(mcp_session_to_release) del associate_context["runtime_objects"]["mcp_session_group"] # --- END: Modified code --- + + # Instrumentation: Track flow finalization in dispatch history + team_state_from_refs = associate_context.get('refs', {}).get('team', {}) + if history_entry := next((h for h in reversed(team_state_from_refs.get("dispatch_history", [])) if h.get("dispatch_id") == executing_associate_id), None): + history_entry["_finally_block_executed"] = True + history_entry["_finally_block_at"] = datetime.now(timezone.utc).isoformat() + # Removed all mcp_session_group cleanup logic executing_associate_id_for_log = associate_context.get("meta", {}).get("agent_id", "UnknownAssociate") current_run_id_for_log = associate_context.get('meta', {}).get('run_id', 'UnknownRun') diff --git a/core/agent_core/framework/context_admission_controller.py b/core/agent_core/framework/context_admission_controller.py new file mode 100644 index 0000000..b60fd72 --- /dev/null +++ b/core/agent_core/framework/context_admission_controller.py @@ -0,0 +1,476 @@ +""" +Context Admission Controller - Pre-admission budget enforcement for tool results. + +Ensures tool results don't spike context past WARNING threshold, giving agents +the opportunity to respond to budget warnings before hitting CRITICAL/EXCEEDED. + +This prevents scenarios where a single large tool result (e.g., web_search +returning 108 items) can jump context from 18% to 94%, bypassing all warning +thresholds and leaving the agent no chance to wrap up gracefully. +""" + +import logging +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass, field + +from .context_budget_guardian import ( + WARNING_THRESHOLD, + get_model_context_limit +) + + +# Module-level cache for default model to avoid repeated lookups +_default_model_cache: Optional[str] = None + + +def _get_default_model() -> str: + """Get a default model for token counting when none specified.""" + global _default_model_cache + if _default_model_cache is None: + # Use Claude as default since it's the primary model in CommonGround + _default_model_cache = "claude-sonnet-4-20250514" + return _default_model_cache + + +def estimate_tokens(text: str, model: Optional[str] = None) -> int: + """ + Count tokens in text using the provider-appropriate token counter. + + Uses the token_counter module which provides accurate counts via: + - Anthropic's official count_tokens API for Claude models + - tiktoken for OpenAI models + - litellm fallback for other providers + + Args: + text: Input text to count tokens for + model: Optional model name for accurate provider-specific counting. + If not provided, uses a default model. + + Returns: + Token count + """ + if not text: + return 0 + + try: + from ..llm.token_counter import count_tokens + model_to_use = model or _get_default_model() + return count_tokens(model=model_to_use, text=text) + except Exception as e: + # Fallback to heuristic if token_counter fails + logger.warning("token_counter_fallback", extra={ + "error": str(e), + "text_length": len(text) + }) + # ~4 chars per token is a reasonable fallback for English text + return max(1, len(text) // 4) + +logger = logging.getLogger(__name__) + + +@dataclass +class AdmissionDecision: + """Result of pre-admission check.""" + admit_full: bool # True if result fits without truncation + admitted_content: Dict # Content to add to context + deferred_content: Optional[List] = None # Content stored in KB for later + deferred_kb_tokens: List[str] = field(default_factory=list) # KB tokens for deferred + truncation_notice: Optional[str] = None # Notice for agent about truncation + + # Metrics + original_tokens: int = 0 + admitted_tokens: int = 0 + deferred_tokens: int = 0 + post_admission_utilization: float = 0.0 + + +# Target utilization after admitting tool results +# Set slightly below WARNING to give agent breathing room +ADMISSION_TARGET_UTILIZATION = 0.38 # 38%, just under WARNING at 40% + +# Minimum tokens to always admit (don't block small results) +MIN_ADMISSION_TOKENS = 5000 + +# Maximum single-item size before considering chunking +MAX_SINGLE_ITEM_TOKENS = 20000 + + +def calculate_admission_budget( + current_tokens: int, + context_limit: int, + target_utilization: float = ADMISSION_TARGET_UTILIZATION +) -> int: + """ + Calculate how many tokens can be admitted while staying under target utilization. + + Args: + current_tokens: Current context token count + context_limit: Model's context window limit + target_utilization: Target utilization after admission (default 38%) + + Returns: + Maximum tokens that can be admitted + """ + target_tokens = int(context_limit * target_utilization) + available = target_tokens - current_tokens + + # Ensure at least some minimum admission (don't block all results) + return max(available, MIN_ADMISSION_TOKENS) + + +def check_pre_admission( + tool_result: Dict, + current_context_tokens: int, + model_name: str, + llm_config: Optional[Dict] = None, + agent_id: Optional[str] = None +) -> AdmissionDecision: + """ + Check if tool result can be admitted without exceeding WARNING threshold. + + If result would push context past WARNING: + 1. Truncate/prioritize content to fit within budget + 2. Store excess in KB with tokens for later expansion + 3. Add truncation notice for agent + + Args: + tool_result: The tool's exec_async result + current_context_tokens: Current context window usage + model_name: Model identifier for context limit lookup + llm_config: Optional LLM config + agent_id: Agent ID for logging + + Returns: + AdmissionDecision with admitted/deferred content + """ + context_limit = get_model_context_limit(model_name, llm_config) + admission_budget = calculate_admission_budget(current_context_tokens, context_limit) + + # Get content to evaluate + payload = tool_result.get("payload", {}) + kb_items = tool_result.get("_knowledge_items_to_add", []) + + # Calculate token counts using provider-specific counter + payload_str = _serialize_payload(payload) + payload_tokens = estimate_tokens(payload_str, model=model_name) + + kb_content_tokens = 0 + for item in kb_items: + content = item.get("content", "") + if isinstance(content, str): + kb_content_tokens += estimate_tokens(content, model=model_name) + + total_result_tokens = payload_tokens + kb_content_tokens + + # Check if full admission is possible + post_admission_tokens = current_context_tokens + total_result_tokens + post_admission_utilization = post_admission_tokens / context_limit if context_limit > 0 else 1.0 + + if post_admission_utilization <= WARNING_THRESHOLD: + # Full admission - no truncation needed + logger.debug("pre_admission_full_admit", extra={ + "agent_id": agent_id, + "result_tokens": total_result_tokens, + "post_utilization": f"{post_admission_utilization:.1%}" + }) + return AdmissionDecision( + admit_full=True, + admitted_content=tool_result, + deferred_content=None, + deferred_kb_tokens=[], + truncation_notice=None, + original_tokens=total_result_tokens, + admitted_tokens=total_result_tokens, + deferred_tokens=0, + post_admission_utilization=post_admission_utilization + ) + + # Truncation needed + logger.warning("pre_admission_truncation_required", extra={ + "agent_id": agent_id, + "original_tokens": total_result_tokens, + "budget_tokens": admission_budget, + "current_utilization": f"{current_context_tokens/context_limit:.1%}" if context_limit > 0 else "N/A", + "would_be_utilization": f"{post_admission_utilization:.1%}" + }) + + return _truncate_result( + tool_result=tool_result, + kb_items=kb_items, + payload=payload, + payload_tokens=payload_tokens, + admission_budget=admission_budget, + current_context_tokens=current_context_tokens, + context_limit=context_limit, + agent_id=agent_id, + model_name=model_name + ) + + +def _serialize_payload(payload: Any) -> str: + """Convert payload to string for token estimation.""" + if payload is None: + return "" + if isinstance(payload, str): + return payload + if isinstance(payload, dict): + # Convert dict to JSON-like string representation + import json + try: + return json.dumps(payload, default=str) + except (TypeError, ValueError): + return str(payload) + return str(payload) + + +def _truncate_result( + tool_result: Dict, + kb_items: List[Dict], + payload: Any, + payload_tokens: int, + admission_budget: int, + current_context_tokens: int, + context_limit: int, + agent_id: Optional[str], + model_name: Optional[str] = None +) -> AdmissionDecision: + """ + Truncate tool result to fit within admission budget. + + Strategy: + 1. Always admit the payload (it's usually small and essential) + 2. For KB items: prioritize by relevance signals, admit top N + 3. Defer remaining KB items (still stored, just not in context) + """ + # Reserve space for payload (minimum 2K tokens or actual size) + payload_budget = min(payload_tokens, max(2000, admission_budget // 4)) + kb_budget = max(0, admission_budget - payload_budget) + + # If no KB items, just return with payload + if not kb_items: + post_utilization = (current_context_tokens + payload_tokens) / context_limit if context_limit > 0 else 1.0 + return AdmissionDecision( + admit_full=True, # No KB items to defer + admitted_content=tool_result, + deferred_content=None, + deferred_kb_tokens=[], + truncation_notice=None, + original_tokens=payload_tokens, + admitted_tokens=payload_tokens, + deferred_tokens=0, + post_admission_utilization=post_utilization + ) + + # Sort KB items by quality signals + scored_items = _score_and_sort_kb_items(kb_items, model=model_name) + + # Admit items until budget exhausted + admitted_items = [] + deferred_items = [] + admitted_kb_tokens = 0 + + for item, score in scored_items: + content = item.get("content", "") + item_tokens = estimate_tokens(content, model=model_name) if isinstance(content, str) else 0 + + if admitted_kb_tokens + item_tokens <= kb_budget: + admitted_items.append(item) + admitted_kb_tokens += item_tokens + else: + # Defer this item - store in KB but don't include in context + deferred_items.append(item) + + # Build deferred KB tokens list + deferred_kb_tokens = [] + for i, item in enumerate(deferred_items): + token = item.get("token") or item.get("item_id") or f"<#CGKB-DEFERRED-{i}>" + deferred_kb_tokens.append(token) + + # Build truncation notice + truncation_notice = None + if deferred_items: + deferred_token_count = sum( + estimate_tokens(item.get("content", ""), model=model_name) + for item in deferred_items + if isinstance(item.get("content"), str) + ) + display_tokens = deferred_kb_tokens[:5] + truncation_notice = ( + f"\n\n---\n" + f"⚠️ **Context Budget Notice**: {len(deferred_items)} additional items " + f"(~{deferred_token_count:,} tokens) were stored in the knowledge base " + f"but not included in context to prevent overflow.\n\n" + f"**Available KB tokens for expansion**: `{', '.join(display_tokens)}`" + f"{'...' if len(deferred_kb_tokens) > 5 else ''}\n\n" + f"Use these tokens with the knowledge base tools if you need the additional content.\n" + f"---" + ) + + # Build modified result + modified_result = tool_result.copy() + modified_result["_knowledge_items_to_add"] = admitted_items + modified_result["_deferred_items"] = deferred_items + modified_result["_deferred_kb_tokens"] = deferred_kb_tokens + + # Add notice to payload if possible + modified_payload = _add_truncation_notice_to_payload(payload, truncation_notice) + if modified_payload is not None: + modified_result["payload"] = modified_payload + + # Calculate final metrics + admitted_tokens = payload_tokens + admitted_kb_tokens + deferred_tokens = sum( + estimate_tokens(item.get("content", "")) + for item in deferred_items + if isinstance(item.get("content"), str) + ) + post_utilization = (current_context_tokens + admitted_tokens) / context_limit if context_limit > 0 else 1.0 + + logger.info("pre_admission_truncation_complete", extra={ + "agent_id": agent_id, + "items_admitted": len(admitted_items), + "items_deferred": len(deferred_items), + "admitted_tokens": admitted_tokens, + "deferred_tokens": deferred_tokens, + "post_utilization": f"{post_utilization:.1%}" + }) + + return AdmissionDecision( + admit_full=False, + admitted_content=modified_result, + deferred_content=deferred_items, + deferred_kb_tokens=deferred_kb_tokens, + truncation_notice=truncation_notice, + original_tokens=admitted_tokens + deferred_tokens, + admitted_tokens=admitted_tokens, + deferred_tokens=deferred_tokens, + post_admission_utilization=post_utilization + ) + + +def _add_truncation_notice_to_payload(payload: Any, notice: Optional[str]) -> Optional[Any]: + """Add truncation notice to payload if it's a dict with known fields.""" + if notice is None: + return None + + if not isinstance(payload, dict): + return None + + modified = payload.copy() + + # Try common payload fields + if "instructional_prompt" in modified: + modified["instructional_prompt"] = str(modified["instructional_prompt"]) + notice + return modified + + if "result" in modified: + modified["result"] = str(modified["result"]) + notice + return modified + + if "content" in modified: + modified["content"] = str(modified["content"]) + notice + return modified + + if "message" in modified: + modified["message"] = str(modified["message"]) + notice + return modified + + # Add as a new field if no known field found + modified["_truncation_notice"] = notice + return modified + + +def _score_and_sort_kb_items(items: List[Dict], model: Optional[str] = None) -> List[Tuple[Dict, float]]: + """ + Score KB items by quality/relevance signals for prioritization. + + Scoring factors: + - Source authority (academic > news > general) + - Content length (prefer substantial content, 500-5000 tokens) + - Relevance signals in metadata + - Position (earlier items may be more relevant) + + Args: + items: List of KB items to score + model: Optional model name for accurate token counting + + Returns: + List of (item, score) tuples sorted by score descending + """ + scored = [] + + for i, item in enumerate(items): + score = 0.0 + content = item.get("content", "") + metadata = item.get("metadata", {}) + source_uri = item.get("source_uri", "") or "" + + # Content substance score (prefer 500-5000 token items) + content_tokens = estimate_tokens(content, model=model) if isinstance(content, str) else 0 + if 500 <= content_tokens <= 5000: + score += 2.0 + elif content_tokens > 5000: + score += 1.0 # Long content is still valuable + elif content_tokens > 100: + score += 0.5 + elif content_tokens < 50: + score -= 1.0 # Very short content is less valuable + + # Source authority score + source_lower = source_uri.lower() + if any(domain in source_lower for domain in ['.gov', '.edu', 'scholar.google', 'pubmed', 'doi.org']): + score += 3.0 + elif any(domain in source_lower for domain in ['nature.com', 'sciencedirect', 'springer', 'wiley', 'jstor']): + score += 2.5 + elif any(domain in source_lower for domain in ['wikipedia', 'britannica', 'who.int']): + score += 1.5 + elif any(domain in source_lower for domain in ['medium.com', 'blog', 'reddit']): + score -= 0.5 # Less authoritative sources + + # Relevance metadata boost + if isinstance(metadata, dict): + if metadata.get("relevance_score"): + try: + score += float(metadata["relevance_score"]) + except (ValueError, TypeError): + pass + + # Boost if explicitly marked as important + if metadata.get("priority") == "high": + score += 1.0 + + # Position penalty (slight preference for earlier items) + # This assumes search results are somewhat ordered by relevance + score -= (i / 100) # -0.01 per position + + scored.append((item, score)) + + # Sort by score descending + scored.sort(key=lambda x: x[1], reverse=True) + return scored + + +def estimate_result_tokens(tool_result: Dict, model: Optional[str] = None) -> int: + """ + Estimate total tokens in a tool result. + + Args: + tool_result: Tool's exec_async result + model: Optional model name for accurate token counting + + Returns: + Token count + """ + payload = tool_result.get("payload", {}) + kb_items = tool_result.get("_knowledge_items_to_add", []) + + payload_str = _serialize_payload(payload) + payload_tokens = estimate_tokens(payload_str, model=model) + + kb_tokens = 0 + for item in kb_items: + content = item.get("content", "") + if isinstance(content, str): + kb_tokens += estimate_tokens(content, model=model) + + return payload_tokens + kb_tokens diff --git a/core/agent_core/framework/context_budget_guardian.py b/core/agent_core/framework/context_budget_guardian.py index fda56ae..fb58bf9 100644 --- a/core/agent_core/framework/context_budget_guardian.py +++ b/core/agent_core/framework/context_budget_guardian.py @@ -8,7 +8,24 @@ Design Principles: - Monitor, don't truncate: We track consumption and trigger early completion - Respect agent autonomy: At warning threshold, inject guidance; at critical, force action +- Respect agent capability: Only force tools that exist in the agent's toolset - Fail gracefully: Even at critical threshold, we guide to completion rather than crash + +Threshold Levels: +- HEALTHY (<60%): Normal operation, no intervention +- WARNING (60-75%): Inject guidance directive suggesting wrap-up +- CRITICAL (75-85%): Force completion for agents with flow-ending tools +- EXCEEDED (>85%): Circuit breaker fires, 15% headroom remains for wrap-up + +Agent-Type-Aware Behavior: +- Principal: Has `finish_flow` β†’ can be forced at CRITICAL/EXCEEDED +- Partner: No flow-ending tools β†’ guidance only, cannot be forced +- Associate: Has `generate_message_summary` β†’ can be forced at CRITICAL/EXCEEDED + +At EXCEEDED threshold: +- Principal: Synthesizes partial results and calls finish_flow +- Partner: Returns user-visible message explaining limit reached +- Associate: Creates handback package for Principal to summarize """ import logging @@ -20,10 +37,10 @@ class ContextBudgetStatus(Enum): """Status levels for context budget consumption.""" - HEALTHY = auto() # < 40% - Normal operation - WARNING = auto() # 40-55% - Inject guidance to wrap up - CRITICAL = auto() # 55-70% - Force immediate completion - EXCEEDED = auto() # > 70% - Circuit breaker, 30% remains for wrap-up + HEALTHY = auto() # < 60% - Normal operation + WARNING = auto() # 60-75% - Inject guidance to wrap up + CRITICAL = auto() # 75-85% - Force immediate completion + EXCEEDED = auto() # > 85% - Circuit breaker, 15% remains for wrap-up # Default context limits by model family (tokens) @@ -49,13 +66,14 @@ class ContextBudgetStatus(Enum): } # Threshold percentages -# These are set to leave 30% headroom for final summarization/wrap-up -WARNING_THRESHOLD = 0.40 # 40% - Start suggesting wrap-up -CRITICAL_THRESHOLD = 0.55 # 55% - Force completion -EXCEEDED_THRESHOLD = 0.70 # 70% - Circuit breaker triggers, 30% remains for wrap-up +# These are set to leave 15% headroom for final summarization/wrap-up +# while allowing agents to make meaningful progress before being interrupted +WARNING_THRESHOLD = 0.60 # 60% - Start suggesting wrap-up +CRITICAL_THRESHOLD = 0.75 # 75% - Force completion +EXCEEDED_THRESHOLD = 0.85 # 85% - Circuit breaker triggers, 15% remains for wrap-up # Budget allocation constants -SUMMARIZATION_RESERVE_PERCENT = 0.30 # Reserve 30% for summarization and wrap-up +SUMMARIZATION_RESERVE_PERCENT = 0.15 # Reserve 15% for summarization and wrap-up def calculate_worker_budget( @@ -246,6 +264,11 @@ def generate_context_budget_directive( For HEALTHY status, returns None (no injection needed). For WARNING/CRITICAL/EXCEEDED, returns a directive to guide the agent. + Agent-type-aware directives: + - Principal: Has `finish_flow` tool - direct to call it + - Partner: Does NOT have `finish_flow` - advise to complete current response + - Associate: Has `generate_message_summary` - direct to call it + Args: status: The current ContextBudgetStatus metadata: Metadata from assess_context_budget @@ -260,13 +283,137 @@ def generate_context_budget_directive( utilization = metadata.get("utilization_percent", 0) remaining = metadata.get("remaining_tokens", 0) - # Select appropriate tool name based on agent type - if agent_type in ("principal", "partner"): - wrap_up_tool = "finish_flow" - wrap_up_action = "conclude your analysis and finalize results" + # Agent-type-specific directives + # Partner does NOT have finish_flow or generate_message_summary tools + if agent_type == "partner": + return _generate_partner_directive(status, utilization, remaining) + elif agent_type == "principal": + return _generate_principal_directive(status, utilization, remaining) else: - wrap_up_tool = "generate_message_summary" - wrap_up_action = "summarize your findings and submit your deliverable" + # Associates have generate_message_summary + return _generate_associate_directive(status, utilization, remaining) + + +def _generate_partner_directive( + status: ContextBudgetStatus, + utilization: float, + remaining: int +) -> str: + """Generate directive for Partner agents (no flow-control tools available).""" + if status == ContextBudgetStatus.WARNING: + return f""" +⚠️ **CONTEXT BUDGET WARNING** ⚠️ + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**Action Required:** +- Begin consolidating your conversation +- Avoid launching new research tasks that would add more context +- Focus on summarizing what has been accomplished so far +- If research is in progress, allow it to complete but plan to wrap up soon + +Continue with your current interaction but prioritize reaching a natural conclusion. +""" + + elif status == ContextBudgetStatus.CRITICAL: + return f""" +🚨 **CRITICAL: CONTEXT BUDGET EXHAUSTED** 🚨 + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**MANDATORY ACTION:** +You must complete your current response concisely and advise the user that: +- The conversation has reached its context limit +- A new conversation may be needed for additional requests + +Do NOT launch any new research or make additional tool calls that would expand context. + +Provide a brief summary of what was accomplished and conclude this interaction. +""" + + elif status == ContextBudgetStatus.EXCEEDED: + return f""" +πŸ›‘ **EMERGENCY: CONTEXT LIMIT EXCEEDED** πŸ›‘ + +Your context utilization is at {utilization}% - the system is at risk of failure. + +**EMERGENCY ACTION:** +Provide a MINIMAL response to the user: +1. Briefly state what was accomplished +2. Inform them that the context limit has been reached +3. Recommend starting a new conversation for further work + +This is your FINAL response opportunity before system failure. +""" + + return None + + +def _generate_principal_directive( + status: ContextBudgetStatus, + utilization: float, + remaining: int +) -> str: + """Generate directive for Principal agents (has finish_flow tool).""" + wrap_up_tool = "finish_flow" + wrap_up_action = "conclude your analysis and finalize results" + + if status == ContextBudgetStatus.WARNING: + return f""" +⚠️ **CONTEXT BUDGET WARNING** ⚠️ + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**Action Required:** +- Begin consolidating your findings +- Avoid dispatching additional submodules +- Plan to call `{wrap_up_tool}` within the next 1-2 turns +- If you have sufficient information, call `{wrap_up_tool}` NOW + +Continue with your current task but prioritize completion. +""" + + elif status == ContextBudgetStatus.CRITICAL: + return f""" +🚨 **CRITICAL: CONTEXT BUDGET EXHAUSTED** 🚨 + +Your context utilization is at {utilization}% ({remaining:,} tokens remaining). + +**MANDATORY ACTION:** +You MUST call `{wrap_up_tool}` tool IMMEDIATELY to {wrap_up_action}. + +Do NOT dispatch any more submodules or make additional queries. Any additional work will cause a system failure. + +{wrap_up_action.capitalize()} NOW. +""" + + elif status == ContextBudgetStatus.EXCEEDED: + return f""" +πŸ›‘ **EMERGENCY: CONTEXT LIMIT EXCEEDED** πŸ›‘ + +Your context utilization is at {utilization}% - the system is at risk of failure. + +**EMERGENCY ACTION:** +Call `{wrap_up_tool}` IMMEDIATELY to {wrap_up_action}. + +Your response must be MINIMAL. Include only: +1. A brief summary of completed work +2. A note that full analysis was interrupted due to context limits + +This is your FINAL opportunity to submit work before system failure. +""" + + return None + + +def _generate_associate_directive( + status: ContextBudgetStatus, + utilization: float, + remaining: int +) -> str: + """Generate directive for Associate agents (has generate_message_summary tool).""" + wrap_up_tool = "generate_message_summary" + wrap_up_action = "summarize your findings and submit your deliverable" if status == ContextBudgetStatus.WARNING: return f""" @@ -324,6 +471,9 @@ def should_force_tool_call(status: ContextBudgetStatus, agent_type: Optional[str force the agent to call the summary tool rather than relying on the directive alone. + NOTE: Only Principal and Associate agents have finish_flow in their toolset. + Partner agents do NOT have finish_flow - they should NOT be forced to call it. + Args: status: The current ContextBudgetStatus agent_type: The agent type ("principal", "partner", "associate", etc.) @@ -332,10 +482,14 @@ def should_force_tool_call(status: ContextBudgetStatus, agent_type: Optional[str Tool name to force, or None if no forced call needed """ if status in (ContextBudgetStatus.CRITICAL, ContextBudgetStatus.EXCEEDED): - # Principal and Partner agents use finish_flow to wrap up - # Associates use generate_message_summary to submit deliverables - if agent_type in ("principal", "partner"): + # Principal agents use finish_flow to wrap up (they have flow_control_end toolset) + if agent_type == "principal": return "finish_flow" + # Partner agents do NOT have finish_flow in their toolset - return None + # They will receive guidance via the context_budget directive but not be forced + if agent_type == "partner": + return None + # Associates use generate_message_summary to submit deliverables return "generate_message_summary" return None diff --git a/core/agent_core/framework/context_budget_handback.py b/core/agent_core/framework/context_budget_handback.py new file mode 100644 index 0000000..09ad40e --- /dev/null +++ b/core/agent_core/framework/context_budget_handback.py @@ -0,0 +1,292 @@ +""" +Context Budget Handback - Data structures for Principal-delegated summarization. + +When a subagent exceeds its context budget, instead of losing all research, +this module packages the collected work for the Principal to summarize. + +The Principal typically has a larger context allocation and can: +1. Expand KB tokens to retrieve the research content +2. Summarize the partial findings itself +3. Decide whether to retry, accept partial, or mark incomplete +""" + +import logging +from dataclasses import dataclass, field, asdict +from datetime import datetime, timezone +from typing import Dict, List, Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class ContextBudgetHandback: + """ + Package returned to Principal when subagent exceeds context budget. + Contains all information needed for Principal to summarize partial work. + """ + # Identification + agent_id: str + module_id: str + profile_name: str + + # Budget state at trigger + utilization_percent: float + predicted_tokens: int + context_limit: int + + # Collected research artifacts + kb_tokens: List[str] = field(default_factory=list) # ["<#CGKB-00029>", ...] + kb_token_count: int = 0 + estimated_kb_content_tokens: int = 0 + + # Tool execution history + tool_calls_completed: List[Dict] = field(default_factory=list) + tool_calls_in_progress: Optional[Dict] = None + + # Partial content + last_assistant_content_preview: str = "" + turns_completed: int = 0 + + # Timestamps + start_timestamp: str = "" + overflow_timestamp: str = "" + + def to_dict(self) -> Dict: + """Convert to dictionary for serialization.""" + return asdict(self) + + @classmethod + def from_dict(cls, data: Dict) -> "ContextBudgetHandback": + """Create from dictionary.""" + return cls(**data) + + def get_principal_summary_prompt(self) -> str: + """Generate a prompt for Principal to summarize the partial work.""" + kb_tokens_display = ', '.join(self.kb_tokens[:20]) + if len(self.kb_tokens) > 20: + kb_tokens_display += f"... (+{len(self.kb_tokens) - 20} more)" + + tool_history = "" + if self.tool_calls_completed: + tool_lines = [] + for i, tc in enumerate(self.tool_calls_completed[:10], 1): + tool_name = tc.get("tool", "unknown") + args_preview = tc.get("arguments_preview", "")[:100] + tool_lines.append(f" {i}. `{tool_name}`: {args_preview}") + tool_history = "\n".join(tool_lines) + if len(self.tool_calls_completed) > 10: + tool_history += f"\n ... (+{len(self.tool_calls_completed) - 10} more)" + else: + tool_history = " (No tool calls completed before overflow)" + + return f""" +## CONTEXT BUDGET HANDBACK - Summarization Required + +Agent `{self.agent_id}` (profile: `{self.profile_name}`) exceeded its context budget +while working on module `{self.module_id}`. + +### Budget Status at Overflow +- **Utilization**: {self.utilization_percent:.1f}% +- **Tokens used**: {self.predicted_tokens:,} / {self.context_limit:,} +- **Turns completed**: {self.turns_completed} + +### Work Completed Before Overflow +- **Tool calls executed**: {len(self.tool_calls_completed)} +- **Knowledge base items collected**: {self.kb_token_count} +- **Estimated content tokens**: ~{self.estimated_kb_content_tokens:,} + +### Tool Call History +{tool_history} + +### Collected KB Tokens +The following KB tokens contain the research data collected before overflow: +``` +{kb_tokens_display} +``` + +### Last Agent Output (Preview) +{self.last_assistant_content_preview if self.last_assistant_content_preview else "(No output captured)"} + +### Your Task as Principal +The subagent's context was too full to self-summarize. You should: + +1. **Expand KB tokens** to retrieve the collected research content + - Use batches that fit your context budget + - Start with the first few tokens to assess content quality + +2. **Summarize key findings** from the expanded content + - Focus on findings relevant to module objective + - Note any incomplete areas + +3. **Decide next steps**: + - **Accept partial**: If findings are sufficient, mark module complete + - **Retry narrower**: Dispatch new agent with more focused scope + - **Mark incomplete**: Proceed with other modules, note gap + +**Recommended action**: Start by expanding 5-10 KB tokens to assess the research quality. +""" + + def get_deliverables_summary(self) -> str: + """Generate a summary for the deliverables field.""" + return ( + f"[CONTEXT BUDGET EXCEEDED] Agent `{self.agent_id}` collected " + f"{self.kb_token_count} KB items before exceeding budget at " + f"{self.utilization_percent:.1f}% utilization. " + f"KB tokens available for Principal summarization: " + f"{', '.join(self.kb_tokens[:5])}{'...' if len(self.kb_tokens) > 5 else ''}" + ) + + +def build_handback_from_context( + agent_id: str, + context: Dict, + prep_res: Dict, + profile_name: str = "unknown" +) -> ContextBudgetHandback: + """ + Build a handback package from agent context when circuit breaker fires. + + Args: + agent_id: The agent's identifier + context: The full agent context + prep_res: The prep_async result containing budget info + profile_name: The agent's profile name + + Returns: + ContextBudgetHandback with all recoverable work packaged + """ + state = context.get("state", {}) + meta = context.get("meta", {}) + budget_info = state.get("_context_budget", {}) + + # Extract KB tokens from this agent's session + kb = context.get('refs', {}).get('run', {}).get('runtime', {}).get("knowledge_base") + agent_kb_tokens = [] + estimated_kb_tokens = 0 + + if kb: + # Get all KB items added by this agent + items_dict = getattr(kb, 'items', {}) + if callable(items_dict): + items_dict = {} # Fallback if items is a method + + for item_id, item in items_dict.items(): + item_metadata = item.get("metadata", {}) if isinstance(item, dict) else {} + if item_metadata.get("source_agent_id") == agent_id: + token = item.get("token", item_id) if isinstance(item, dict) else item_id + agent_kb_tokens.append(token) + # Rough estimate: 4 chars per token + content = item.get("content", "") if isinstance(item, dict) else "" + estimated_kb_tokens += len(content) // 4 + + # Extract tool call history from messages + tool_calls_completed = [] + messages = state.get("messages", []) + for msg in messages: + if msg.get("role") == "assistant" and msg.get("tool_calls"): + for tc in msg["tool_calls"]: + func = tc.get("function", {}) + tool_calls_completed.append({ + "tool": func.get("name"), + "arguments_preview": str(func.get("arguments", ""))[:200], + "tool_call_id": tc.get("id", "") + }) + + # Get last assistant content + last_content = "" + for msg in reversed(messages): + if msg.get("role") == "assistant" and msg.get("content"): + content = msg["content"] + if isinstance(content, str): + last_content = content[:500] + break + + # Get module ID from various sources + module_id = ( + meta.get("module_id") or + state.get("initial_parameters", {}).get("module_id") or + meta.get("agent_id", "unknown") + ) + + handback = ContextBudgetHandback( + agent_id=agent_id, + module_id=module_id, + profile_name=profile_name, + utilization_percent=budget_info.get("utilization_percent", 0), + predicted_tokens=prep_res.get("predicted_total_tokens", 0), + context_limit=budget_info.get("context_limit", 200000), + kb_tokens=agent_kb_tokens, + kb_token_count=len(agent_kb_tokens), + estimated_kb_content_tokens=estimated_kb_tokens, + tool_calls_completed=tool_calls_completed, + tool_calls_in_progress=state.get("current_action"), + last_assistant_content_preview=last_content, + turns_completed=len([m for m in messages if m.get("role") == "assistant"]), + start_timestamp=meta.get("start_timestamp", ""), + overflow_timestamp=datetime.now(timezone.utc).isoformat() + ) + + logger.info("context_budget_handback_built", extra={ + "agent_id": agent_id, + "module_id": module_id, + "kb_tokens_collected": len(agent_kb_tokens), + "tool_calls_completed": len(tool_calls_completed), + "utilization_percent": budget_info.get("utilization_percent", 0) + }) + + return handback + + +def notify_principal_of_handback(principal_context: Dict, handback: ContextBudgetHandback) -> None: + """ + Inject handback notification into Principal's inbox. + + Args: + principal_context: The Principal agent's context + handback: The handback package from the overflowed subagent + """ + import uuid + + notification = { + "item_id": f"handback_{handback.agent_id}_{uuid.uuid4().hex[:8]}", + "source": "CONTEXT_BUDGET_HANDBACK", + "payload": { + "content_key": "principal_handback_notification", + "handback": handback.to_dict(), + "message": f""" +## πŸ”„ Context Budget Handback from `{handback.agent_id}` + +Module `{handback.module_id}` exceeded context budget at **{handback.utilization_percent:.1f}%** utilization. + +**Research Collected (Available for Your Summarization):** +- **{handback.kb_token_count}** knowledge base items +- **{len(handback.tool_calls_completed)}** tool calls completed +- Estimated **~{handback.estimated_kb_content_tokens:,}** tokens of content + +**KB Tokens to Expand:** +``` +{', '.join(handback.kb_tokens[:10])}{'...' if len(handback.kb_tokens) > 10 else ''} +``` + +**Recommended Action:** +Expand the KB tokens above to retrieve the collected research, then summarize the key findings yourself. +This agent's context was too full to self-summarize. +""" + }, + "consumption_policy": "preserve", + "metadata": { + "created_at": datetime.now(timezone.utc).isoformat(), + "priority": "high", + "requires_action": True, + "handback_agent_id": handback.agent_id, + "handback_module_id": handback.module_id + } + } + + principal_context["state"].setdefault("inbox", []).append(notification) + + logger.info("principal_handback_notification_sent", extra={ + "agent_id": handback.agent_id, + "module_id": handback.module_id, + "kb_tokens": handback.kb_token_count + }) diff --git a/core/agent_core/framework/turn_manager.py b/core/agent_core/framework/turn_manager.py index 3db04d4..70c99a7 100644 --- a/core/agent_core/framework/turn_manager.py +++ b/core/agent_core/framework/turn_manager.py @@ -3,7 +3,7 @@ import uuid import json from datetime import datetime, timezone -from typing import Dict, Any, Optional, List +from typing import Dict, Any, Optional, List, Tuple # Import Turn model definitions from ..models.turn import Turn, LLMInteraction, ToolInteraction @@ -11,6 +11,10 @@ # Get logger logger = logging.getLogger(__name__) +# Constants for orphan detection +ORPHAN_TOOL_INTERACTION_TIMEOUT_SECONDS = 300 # 5 minutes + + class TurnManager: """ A service class dedicated to managing the lifecycle of an Agent Turn. @@ -379,3 +383,79 @@ def create_aggregation_turn( logger.info("aggregation_turn_created_by_manager", extra={"aggregation_turn_id": aggregation_turn_id, "dispatch_tool_call_id": dispatch_tool_call_id}) return aggregation_turn_id + + def detect_orphaned_tool_interactions(self, team_state: Dict, timeout_seconds: int = ORPHAN_TOOL_INTERACTION_TIMEOUT_SECONDS) -> List[Tuple[str, str, Dict]]: + """ + Detects tool interactions that are stuck in "running" state for longer than the timeout. + + This helps identify silent failures where tools crashed before sending results back to the inbox. + + Args: + team_state: The shared team_state dictionary. + timeout_seconds: How long a tool can be in "running" state before being considered orphaned. + + Returns: + List of tuples: (turn_id, tool_call_id, tool_interaction_dict) + """ + if "turns" not in team_state: + return [] + + orphaned = [] + now = datetime.now(timezone.utc) + + for turn in team_state["turns"]: + for ti in turn.get("tool_interactions", []): + if ti.get("status") == "running": + start_time_str = ti.get("start_time") + if start_time_str: + try: + start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) + elapsed = (now - start_time).total_seconds() + if elapsed > timeout_seconds: + orphaned.append((turn.get("turn_id"), ti.get("tool_call_id"), ti)) + logger.warning("orphaned_tool_interaction_detected", extra={ + "turn_id": turn.get("turn_id"), + "tool_call_id": ti.get("tool_call_id"), + "tool_name": ti.get("tool_name"), + "elapsed_seconds": elapsed, + "start_time": start_time_str + }) + except (ValueError, TypeError) as e: + logger.debug("orphan_detection_time_parse_error", extra={ + "tool_call_id": ti.get("tool_call_id"), + "error": str(e) + }) + + return orphaned + + def finalize_orphaned_tool_interactions(self, team_state: Dict, timeout_seconds: int = ORPHAN_TOOL_INTERACTION_TIMEOUT_SECONDS) -> int: + """ + Detects and finalizes orphaned tool interactions by marking them as errors. + + This is a recovery mechanism to prevent silent failures from leaving the system in an inconsistent state. + + Args: + team_state: The shared team_state dictionary. + timeout_seconds: How long a tool can be in "running" state before being considered orphaned. + + Returns: + Number of orphaned tool interactions that were finalized. + """ + orphaned = self.detect_orphaned_tool_interactions(team_state, timeout_seconds) + finalized_count = 0 + + for turn_id, tool_call_id, ti in orphaned: + ti["status"] = "error" + ti["end_time"] = datetime.now(timezone.utc).isoformat() + ti["error_details"] = f"Tool interaction timed out after {timeout_seconds}s - possible silent failure" + ti["result_payload"] = {"error": "orphaned_tool_interaction", "timeout_seconds": timeout_seconds} + finalized_count += 1 + + logger.warning("orphaned_tool_interaction_finalized", extra={ + "turn_id": turn_id, + "tool_call_id": tool_call_id, + "tool_name": ti.get("tool_name"), + "timeout_seconds": timeout_seconds + }) + + return finalized_count diff --git a/core/agent_core/nodes/base_agent_node.py b/core/agent_core/nodes/base_agent_node.py index f09f1b0..46043db 100644 --- a/core/agent_core/nodes/base_agent_node.py +++ b/core/agent_core/nodes/base_agent_node.py @@ -17,6 +17,10 @@ should_force_tool_call, synthesize_partial_results ) +from ..framework.context_budget_handback import ( + ContextBudgetHandback, + build_handback_from_context +) import json_repair import os from typing import Dict, Any, Optional, List @@ -759,32 +763,27 @@ async def exec_async(self, prep_res: Dict) -> Dict: # =============================================================== # CIRCUIT BREAKER: Skip LLM call if context budget exceeded + # Create handback for Principal instead of forcing tool call # =============================================================== if prep_res.get("skip_llm_call"): logger.warning( - "exec_async_skipped_due_to_context_budget", + "exec_async_circuit_breaker_handback", extra={"agent_id": self.agent_id, "run_id": run_id} ) - # Synthesize partial results from completed work - team_state = context.get('refs', {}).get('run', {}).get('team_state', {}) + agent_type = prep_res.get("agent_type") or "associate" budget_metadata = context.get('state', {}).get('_context_budget', {}) - synthesis = synthesize_partial_results( - team_state=team_state, - triggered_agent_id=self.agent_id, - budget_metadata=budget_metadata - ) - - # Return a synthetic response that forces flow completion - # Tool selection depends on agent type: - # - Principal/Partner: use finish_flow to wrap up the session - # - Associates: use generate_message_summary to submit deliverables - forced_tool_call_id = f"forced_circuit_breaker_{uuid.uuid4().hex[:8]}" - agent_type = prep_res.get("agent_type") or "associate" # From profile's "type" field - - if agent_type in ("principal", "partner"): - # Principal/Partner should gracefully end the flow with synthesis + if agent_type == "principal": + # Principal: use finish_flow approach (they have flow_control_end toolset) + team_state = context.get('refs', {}).get('run', {}).get('team_state', {}) + synthesis = synthesize_partial_results( + team_state=team_state, + triggered_agent_id=self.agent_id, + budget_metadata=budget_metadata + ) + + forced_tool_call_id = f"forced_circuit_breaker_{uuid.uuid4().hex[:8]}" forced_tool_name = "finish_flow" forced_tool_args = { "reason": f"Context budget exceeded ({budget_metadata.get('utilization_percent', '>70')}% utilization). Circuit breaker triggered.", @@ -792,43 +791,108 @@ async def exec_async(self, prep_res: Dict) -> Dict: "completed_modules": synthesis.get("summary", {}).get("completed", 0), "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) } - else: - # Associates should submit their current findings - forced_tool_name = "generate_message_summary" - forced_tool_args = { - "reason": f"Context budget exceeded ({budget_metadata.get('utilization_percent', '>70')}% utilization). Circuit breaker triggered.", - "partial_work_summary": synthesis.get("user_message", "Work interrupted due to context limits.") + + logger.info( + "circuit_breaker_tool_selected", + extra={ + "agent_id": self.agent_id, + "agent_type": agent_type, + "tool_name": forced_tool_name, + "completed_modules": synthesis.get("summary", {}).get("completed", 0), + "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) + } + ) + + return { + "content": f"[CONTEXT BUDGET EXCEEDED - Automatic {forced_tool_name} triggered]\n\n{synthesis.get('user_message', '')}", + "tool_calls": [{ + "id": forced_tool_call_id, + "type": "function", + "function": { + "name": forced_tool_name, + "arguments": json.dumps(forced_tool_args) + } + }], + "reasoning": None, + "model_id_used": "circuit_breaker", + "error": None, + "circuit_breaker_synthesis": synthesis } - - logger.info( - "circuit_breaker_tool_selected", - extra={ - "agent_id": self.agent_id, - "agent_type": agent_type, - "tool_name": forced_tool_name, - "completed_modules": synthesis.get("summary", {}).get("completed", 0), - "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) + elif agent_type == "partner": + # Partner: does NOT have finish_flow - return a message to the user instead + # Synthesize partial results and return as content without forcing a tool call + team_state = context.get('refs', {}).get('run', {}).get('team_state', {}) + synthesis = synthesize_partial_results( + team_state=team_state, + triggered_agent_id=self.agent_id, + budget_metadata=budget_metadata + ) + + utilization = budget_metadata.get('utilization_percent', '>85') + user_message = ( + f"⚠️ **Context Budget Limit Reached** ({utilization}% utilization)\n\n" + f"I've accumulated too much conversation history to continue safely. " + f"To proceed with your request, please start a new conversation.\n\n" + f"**What was completed:**\n{synthesis.get('user_message', 'Partial results available.')}" + ) + + logger.info( + "circuit_breaker_partner_message", + extra={ + "agent_id": self.agent_id, + "agent_type": agent_type, + "utilization_percent": utilization, + "completed_modules": synthesis.get("summary", {}).get("completed", 0), + "incomplete_modules": synthesis.get("summary", {}).get("incomplete", 0) + } + ) + + return { + "content": user_message, + "tool_calls": [], # No tool call - just return the message + "reasoning": None, + "model_id_used": "circuit_breaker", + "error": None, + "circuit_breaker_synthesis": synthesis } - ) - - # Include the synthesis in the response content for user visibility - synthesis_content = synthesis.get("user_message", "") - - return { - "content": f"[CONTEXT BUDGET EXCEEDED - Automatic {forced_tool_name} triggered]\n\n{synthesis_content}", - "tool_calls": [{ - "id": forced_tool_call_id, - "type": "function", - "function": { - "name": forced_tool_name, - "arguments": json.dumps(forced_tool_args) + else: + # Associates: Build handback for Principal summarization + # This avoids the infinite loop caused by forcing generate_message_summary + profile_name = self.loaded_profile.get("name", "unknown") if self.loaded_profile else "unknown" + handback = build_handback_from_context( + agent_id=self.agent_id, + context=context, + prep_res=prep_res, + profile_name=profile_name + ) + + # Store handback in deliverables for Principal to access + context["state"].setdefault("deliverables", {}) + context["state"]["deliverables"]["_handback"] = handback.to_dict() + context["state"]["deliverables"]["status"] = "CONTEXT_BUDGET_EXCEEDED" + context["state"]["deliverables"]["primary_summary"] = handback.get_deliverables_summary() + + logger.info( + "circuit_breaker_handback_created", + extra={ + "agent_id": self.agent_id, + "agent_type": agent_type, + "kb_tokens_collected": handback.kb_token_count, + "tool_calls_completed": len(handback.tool_calls_completed), + "utilization_percent": handback.utilization_percent } - }], - "reasoning": None, - "model_id_used": "circuit_breaker", - "error": None, - "circuit_breaker_synthesis": synthesis # Include full synthesis for downstream processing - } + ) + + # Return with END_FLOW signal - no tool call to avoid loop + return { + "content": handback.get_principal_summary_prompt(), + "tool_calls": [], # NO tool call - avoid the infinite loop + "reasoning": None, + "model_id_used": "circuit_breaker_handback", + "error": None, + "_flow_action": "END_FLOW", # Signal to post_async + "_handback": handback.to_dict() + } # =============================================================== # Create a placeholder message ID @@ -917,6 +981,28 @@ async def post_async(self, context: Dict, prep_res: Dict, exec_res: Dict) -> str next_action = "error_in_post" try: + # =============================================================== + # CIRCUIT BREAKER HANDBACK: Immediate termination on END_FLOW + # =============================================================== + if exec_res.get("_flow_action") == "END_FLOW": + logger.info( + "circuit_breaker_handback_terminating", + extra={ + "agent_id": self.agent_id, + "has_handback": "_handback" in exec_res + } + ) + # Update assistant message with handback content + self._update_assistant_message_in_state(state, exec_res) + + # Ensure turn is properly finalized + if turn_manager: + turn_manager.update_llm_interaction_end(context, exec_res) + + # Clean termination - no more turns + return "END_FLOW" + # =============================================================== + if "error" in llm_response and llm_response["error"]: error_message = llm_response["error"] logger.error("post_processing_llm_error", extra={"agent_id": self.agent_id, "error_message": error_message}, exc_info=True) diff --git a/core/agent_core/nodes/base_tool_node.py b/core/agent_core/nodes/base_tool_node.py index 1d8430b..c92980e 100644 --- a/core/agent_core/nodes/base_tool_node.py +++ b/core/agent_core/nodes/base_tool_node.py @@ -68,6 +68,7 @@ async def exec_async(self, prep_res: Dict[str, Any]) -> Dict[str, Any]: async def post_async(self, shared: Dict, prep_res: Dict, exec_res: Dict): """ [Refactored] Generic post-processing stage. + - PRE-ADMISSION CHECK: Prevents context spikes from large tool results - Handles knowledge base items declared in exec_res. - Intelligently dehydrates the payload. - Wraps the result in a TOOL_RESULT event. @@ -76,7 +77,84 @@ async def post_async(self, shared: Dict, prep_res: Dict, exec_res: Dict): state = shared.get("state", {}) is_error = exec_res.get("status") == "error" - # --- START: New knowledge base handling logic --- + # =============================================================== + # PRE-ADMISSION BUDGET CHECK: Prevent context spikes + # If tool result would push context past WARNING threshold, + # truncate intelligently and defer excess to KB + # =============================================================== + if not is_error and (exec_res.get("_knowledge_items_to_add") or exec_res.get("payload")): + try: + from ..framework.context_admission_controller import check_pre_admission, estimate_tokens + + # Get model info first - try multiple sources + llm_config = {} + run_refs = shared.get("refs", {}).get("run", {}) + config = run_refs.get("config", {}) + + # Try to get model from llm_configs + llm_configs = config.get("llm_configs", {}) + if isinstance(llm_configs, dict): + # Get the associate config or first available + for key in ["associate_llm", "default", "principal_llm"]: + if key in llm_configs: + llm_config = llm_configs[key] + break + if not llm_config and llm_configs: + llm_config = next(iter(llm_configs.values()), {}) + + model_name = llm_config.get("model", "anthropic/claude-sonnet-4-20250514") + agent_id = shared.get("meta", {}).get("agent_id") + + # Estimate current context tokens from messages using accurate counting + messages = state.get("messages", []) + current_tokens = 0 + for msg in messages: + content = msg.get("content", "") + if isinstance(content, str): + current_tokens += estimate_tokens(content, model=model_name) + # Add overhead for tool calls + if msg.get("tool_calls"): + current_tokens += estimate_tokens(str(msg["tool_calls"]), model=model_name) + + # Check admission + admission = check_pre_admission( + tool_result=exec_res, + current_context_tokens=current_tokens, + model_name=model_name, + llm_config=llm_config, + agent_id=agent_id + ) + + if not admission.admit_full: + # Use truncated result + exec_res = admission.admitted_content + + # Store deferred items in KB with tokens + if admission.deferred_content: + kb = shared.get('refs', {}).get('run', {}).get('runtime', {}).get("knowledge_base") + if kb: + for item in admission.deferred_content: + item.setdefault("metadata", {}) + item["metadata"]["deferred"] = True + item["metadata"]["deferred_reason"] = "context_budget_admission_control" + item["metadata"]["source_tool_name"] = self._tool_info["name"] + item["metadata"]["source_agent_id"] = agent_id + await kb.add_item(item) + + logger.info("tool_result_truncated_for_admission", extra={ + "tool_name": self._tool_info['name'], + "original_tokens": admission.original_tokens, + "admitted_tokens": admission.admitted_tokens, + "deferred_tokens": admission.deferred_tokens, + "deferred_kb_count": len(admission.deferred_kb_tokens) + }) + except ImportError as e: + logger.debug("pre_admission_check_skipped_import", extra={"error": str(e)}) + except Exception as e: + logger.warning("pre_admission_check_failed", extra={"error": str(e)}) + # =============================================================== + + # --- START: Knowledge base handling logic --- knowledge_items_to_add = exec_res.get("_knowledge_items_to_add", []) if knowledge_items_to_add and not is_error: kb = shared.get('refs', {}).get('run', {}).get('runtime', {}).get("knowledge_base") diff --git a/core/agent_core/nodes/custom_nodes/dispatcher_node.py b/core/agent_core/nodes/custom_nodes/dispatcher_node.py index 292664e..4b238b4 100644 --- a/core/agent_core/nodes/custom_nodes/dispatcher_node.py +++ b/core/agent_core/nodes/custom_nodes/dispatcher_node.py @@ -26,6 +26,117 @@ logger = logging.getLogger(__name__) +# Constants for dispatch health monitoring +DISPATCH_TIMEOUT_SECONDS = 600 # 10 minutes - if a dispatch is RUNNING longer, it may be stuck + + +def detect_stuck_dispatches(team_state: Dict, timeout_seconds: int = DISPATCH_TIMEOUT_SECONDS) -> List[Dict]: + """ + Detects dispatches that are stuck in RUNNING or LAUNCHING state for longer than the timeout. + + This helps identify silent failures where Associate subflows crashed without proper cleanup. + + Args: + team_state: The shared team_state dictionary. + timeout_seconds: How long a dispatch can be RUNNING before being considered stuck. + + Returns: + List of stuck dispatch entries. + """ + dispatch_history = team_state.get("dispatch_history", []) + if not dispatch_history: + return [] + + stuck = [] + now = datetime.now(timezone.utc) + + for entry in dispatch_history: + status = entry.get("status", "").upper() + if status in ["RUNNING", "LAUNCHING"]: + # Check start time + start_time_str = entry.get("start_timestamp") or entry.get("_dispatch_started_at") + if start_time_str: + try: + start_time = datetime.fromisoformat(start_time_str.replace("Z", "+00:00")) + elapsed = (now - start_time).total_seconds() + if elapsed > timeout_seconds: + stuck.append(entry) + logger.warning("stuck_dispatch_detected", extra={ + "dispatch_id": entry.get("dispatch_id"), + "module_id": entry.get("module_id"), + "status": status, + "elapsed_seconds": elapsed, + "start_timestamp": start_time_str, + "_subcontext_created": entry.get("_subcontext_created", "unknown"), + "_associate_flow_started": entry.get("_associate_flow_started", "unknown"), + "_associate_flow_completed": entry.get("_associate_flow_completed", "unknown"), + }) + except (ValueError, TypeError) as e: + logger.debug("stuck_dispatch_time_parse_error", extra={ + "dispatch_id": entry.get("dispatch_id"), + "error": str(e) + }) + + return stuck + + +def get_dispatch_health_report(team_state: Dict) -> Dict[str, Any]: + """ + Generates a health report for all dispatches in team_state. + + Returns: + Dict with health metrics and anomalies detected. + """ + dispatch_history = team_state.get("dispatch_history", []) + + report = { + "total_dispatches": len(dispatch_history), + "by_status": {}, + "anomalies": [], + "lifecycle_incomplete": [], + } + + for entry in dispatch_history: + status = entry.get("status", "UNKNOWN") + report["by_status"][status] = report["by_status"].get(status, 0) + 1 + + # Check for lifecycle anomalies (instrumentation fields) + subcontext_created = entry.get("_subcontext_created", None) + flow_started = entry.get("_associate_flow_started", None) + flow_completed = entry.get("_associate_flow_completed", None) + + # Detect incomplete lifecycles + if subcontext_created is not None: # Has instrumentation + issues = [] + if subcontext_created and not flow_started: + issues.append("subcontext_created_but_flow_not_started") + if flow_started and not flow_completed: + issues.append("flow_started_but_not_completed") + if entry.get("_critical_error"): + issues.append(f"critical_error: {entry.get('_critical_error_type', 'unknown')}") + if entry.get("_flow_error"): + issues.append(f"flow_error: {entry.get('_flow_error_type', 'unknown')}") + + if issues: + report["lifecycle_incomplete"].append({ + "dispatch_id": entry.get("dispatch_id"), + "module_id": entry.get("module_id"), + "status": status, + "issues": issues, + }) + + # Detect stuck dispatches + stuck = detect_stuck_dispatches(team_state) + if stuck: + report["anomalies"].append({ + "type": "stuck_dispatches", + "count": len(stuck), + "dispatch_ids": [s.get("dispatch_id") for s in stuck] + }) + + return report + + DESCRIPTION = """ Called by the Principal to validate and assign a Work Module to an Associate Agent for execution. - assignments: List of assignments to be made. Each assignment targets **one** Work Module. @@ -361,7 +472,12 @@ async def exec_async(self, assignment_package: Dict) -> Dict: history_entry = { "dispatch_id": executing_associate_id, "dispatch_tool_call_id_ref": assignment_package["dispatch_tool_call_id_ref"], "module_id": module_id, "profile_logical_name": profile_logical_name, "start_timestamp": None, - "end_timestamp": None, "status": "LAUNCHING", "final_summary": None, "error_details": None + "end_timestamp": None, "status": "LAUNCHING", "final_summary": None, "error_details": None, + # Instrumentation: Track dispatch lifecycle for debugging silent failures + "_dispatch_started_at": datetime.now(timezone.utc).isoformat(), + "_subcontext_created": False, + "_associate_flow_started": False, + "_associate_flow_completed": False, } team_state_global.setdefault("dispatch_history", []).append(history_entry) logger.info("dispatcher_history_entry_added", extra={"executing_associate_id": executing_associate_id, "status": "LAUNCHING"}) @@ -459,6 +575,11 @@ async def exec_async(self, assignment_package: Dict) -> Dict: logger.info("dispatcher_associate_starting", extra={"executing_associate_id": executing_associate_id}) + # Update history entry to track subcontext creation + if history_entry_to_update := next((h for h in team_state_global.get("dispatch_history", []) if h.get("dispatch_id") == executing_associate_id), None): + history_entry_to_update["_subcontext_created"] = True + history_entry_to_update["_subcontext_created_at"] = datetime.now(timezone.utc).isoformat() + completed_associate_context = None associate_exec_status = "error" last_turn_id = None @@ -467,6 +588,12 @@ async def exec_async(self, assignment_package: Dict) -> Dict: if run_context_global: run_context_global['sub_context_refs']["_ongoing_associate_tasks"][executing_associate_id] = associate_sub_context logger.info("dispatcher_associate_task_registered", extra={"executing_associate_id": executing_associate_id}) + + # Update history entry to track flow start + if history_entry_to_update := next((h for h in team_state_global.get("dispatch_history", []) if h.get("dispatch_id") == executing_associate_id), None): + history_entry_to_update["_associate_flow_started"] = True + history_entry_to_update["_associate_flow_started_at"] = datetime.now(timezone.utc).isoformat() + from ...flow import run_associate_async completed_associate_context = await run_associate_async(associate_sub_context) @@ -476,7 +603,21 @@ async def exec_async(self, assignment_package: Dict) -> Dict: if not final_associate_state.get("error_message"): associate_exec_status = "success" except Exception as e: - logger.error("dispatcher_associate_critical_error", extra={"executing_associate_id": executing_associate_id, "error": str(e)}, exc_info=True) + logger.error("dispatcher_associate_critical_error", extra={ + "executing_associate_id": executing_associate_id, + "error": str(e), + "error_type": type(e).__name__, + "module_id": module_id, + "profile": profile_logical_name, + }, exc_info=True) + + # Update history entry to track the failure point + if history_entry_to_update := next((h for h in team_state_global.get("dispatch_history", []) if h.get("dispatch_id") == executing_associate_id), None): + history_entry_to_update["_critical_error"] = True + history_entry_to_update["_critical_error_at"] = datetime.now(timezone.utc).isoformat() + history_entry_to_update["_critical_error_type"] = type(e).__name__ + history_entry_to_update["_critical_error_message"] = str(e)[:500] # Truncate for storage + if completed_associate_context is None: completed_associate_context = {} final_associate_state = completed_associate_context.setdefault("state", {}) final_associate_state["error_message"] = f"Dispatcher critical error: {str(e)}" @@ -485,6 +626,12 @@ async def exec_async(self, assignment_package: Dict) -> Dict: finally: end_time_iso = datetime.now(timezone.utc).isoformat() final_outcome = "completed_success" if associate_exec_status == "success" else "completed_error" + + # Update history entry to track flow completion + if history_entry_to_update := next((h for h in team_state_global.get("dispatch_history", []) if h.get("dispatch_id") == executing_associate_id), None): + history_entry_to_update["_associate_flow_completed"] = True + history_entry_to_update["_associate_flow_completed_at"] = end_time_iso + history_entry_to_update["_final_outcome"] = final_outcome final_associate_state = completed_associate_context.get("state", {}) if completed_associate_context else {} deliverables_from_associate = final_associate_state.get("deliverables", {}) @@ -703,3 +850,87 @@ async def run_batch_async(self, shared: Dict, prep_res_list: List[Dict]) -> List else: processed_exec_res_list.append(res_or_exc) return processed_exec_res_list + + +def detect_dispatch_anomalies(shared_state, stale_threshold_minutes: int = 60) -> List[Dict[str, Any]]: + """ + Detects anomalies in dispatch history that may indicate silent failures. + + This function helps identify dispatches that: + 1. Are stuck in RUNNING state for too long (stale dispatches) + 2. Have RUNNING status but the corresponding work module has no sub_context + + Args: + shared_state: The shared state object containing team_state + stale_threshold_minutes: Number of minutes after which a RUNNING dispatch is considered stale + + Returns: + List of anomaly dicts with details about each detected anomaly + """ + anomalies = [] + + # Handle both dict-style and object-style access + if hasattr(shared_state, 'team_state'): + team_state = shared_state.team_state + elif isinstance(shared_state, dict): + team_state = shared_state.get('team_state', {}) + else: + team_state = {} + + if not team_state: + return anomalies + + dispatch_history = team_state.get("dispatch_history", []) + work_modules = team_state.get("work_modules", {}) + now = datetime.now(timezone.utc) + stale_threshold_seconds = stale_threshold_minutes * 60 + + for dispatch in dispatch_history: + dispatch_id = dispatch.get("dispatch_id", "unknown") + module_id = dispatch.get("module_id", "unknown") + status = dispatch.get("status", "").upper() + start_timestamp_str = dispatch.get("start_timestamp") + end_timestamp = dispatch.get("end_timestamp") + + # Check for stale RUNNING dispatches + if status == "RUNNING" and not end_timestamp: + if start_timestamp_str: + try: + start_time = datetime.fromisoformat(start_timestamp_str.replace("Z", "+00:00")) + elapsed_seconds = (now - start_time).total_seconds() + + if elapsed_seconds > stale_threshold_seconds: + # Check if work module has a sub_context + work_module = work_modules.get(module_id, {}) + has_sub_context = work_module.get("sub_context_id") is not None + + anomaly = { + "dispatch_id": dispatch_id, + "module_id": module_id, + "anomaly_type": "stale_running", + "status": status, + "elapsed_minutes": round(elapsed_seconds / 60, 1), + "start_timestamp": start_timestamp_str, + "has_sub_context": has_sub_context, + "details": f"Dispatch has been RUNNING for {round(elapsed_seconds / 60, 1)} minutes without completion. " + f"Work module {'has' if has_sub_context else 'has no'} sub_context." + } + + if not has_sub_context: + anomaly["details"] += " No sub_context was created - dispatch may have failed silently." + + anomalies.append(anomaly) + logger.warning("dispatch_anomaly_detected", extra={ + "dispatch_id": dispatch_id, + "module_id": module_id, + "anomaly_type": "stale_running", + "elapsed_minutes": round(elapsed_seconds / 60, 1), + "has_sub_context": has_sub_context + }) + except (ValueError, TypeError) as e: + logger.debug("dispatch_anomaly_time_parse_error", extra={ + "dispatch_id": dispatch_id, + "error": str(e) + }) + + return anomalies diff --git a/core/agent_core/nodes/custom_nodes/jina_search_node.py b/core/agent_core/nodes/custom_nodes/jina_search_node.py index 22b099c..71231f3 100644 --- a/core/agent_core/nodes/custom_nodes/jina_search_node.py +++ b/core/agent_core/nodes/custom_nodes/jina_search_node.py @@ -47,6 +47,7 @@ async def _fetch_single_query(self, query: str, purpose: str): jina_key = get_jina_key() if not jina_key: error_message = "JINA_KEY environment variable is not set" + logger.error("jina_api_key_missing", extra={"query": query}) else: api_url = f'https://s.jina.ai/?q={query}' headers = {'Authorization': f'Bearer {jina_key}', 'X-Respond-With': 'no-content', 'Accept': 'application/json'} @@ -61,10 +62,32 @@ async def _fetch_single_query(self, query: str, purpose: str): else: error_text = await response.text() error_message = f"Search engine returned an error: HTTP {response.status} - {error_text}" + # Log with appropriate severity based on error type + if response.status == 402: + logger.error("jina_api_insufficient_balance", extra={ + "query": query, + "http_status": response.status, + "error_detail": error_text, + "action_required": "Recharge Jina API account at https://jina.ai" + }) + elif response.status == 429: + logger.warning("jina_api_rate_limited", extra={ + "query": query, + "http_status": response.status, + "error_detail": error_text + }) + else: + logger.error("jina_api_error", extra={ + "query": query, + "http_status": response.status, + "error_detail": error_text + }) except asyncio.TimeoutError: error_message = "Search query timed out" + logger.warning("jina_api_timeout", extra={"query": query}) except Exception as e: error_message = f"Search error: {str(e)}" + logger.error("jina_api_exception", extra={"query": query, "error": str(e)}) # Prepare content for the LLM main_content_for_llm = { diff --git a/core/agent_core/nodes/custom_nodes/jina_visit_node.py b/core/agent_core/nodes/custom_nodes/jina_visit_node.py index 510a8d1..24ee0b3 100644 --- a/core/agent_core/nodes/custom_nodes/jina_visit_node.py +++ b/core/agent_core/nodes/custom_nodes/jina_visit_node.py @@ -70,6 +70,7 @@ async def _fetch_single_url(self, url: str, purpose: str, context: str): jina_key = get_jina_key() if not jina_key: error_message = "JINA_KEY environment variable is not set" + logger.error("jina_api_key_missing", extra={"url": url}) else: headers = {"Authorization": f"Bearer {jina_key}"} async with aiohttp.ClientSession() as session: @@ -91,11 +92,34 @@ async def _fetch_single_url(self, url: str, purpose: str, context: str): except Exception: pass success_flag = True else: + error_text = await response.text() error_message = f"Failed to visit, status code: {response.status}" + # Log with appropriate severity based on error type + if response.status == 402: + logger.error("jina_api_insufficient_balance", extra={ + "url": url, + "http_status": response.status, + "error_detail": error_text, + "action_required": "Recharge Jina API account at https://jina.ai" + }) + elif response.status == 429: + logger.warning("jina_api_rate_limited", extra={ + "url": url, + "http_status": response.status, + "error_detail": error_text + }) + else: + logger.error("jina_api_error", extra={ + "url": url, + "http_status": response.status, + "error_detail": error_text + }) except asyncio.TimeoutError: error_message = "Timeout when visiting URL" + logger.warning("jina_api_timeout", extra={"url": url}) except Exception as e: error_message = f"Error visiting URL: {str(e)}" + logger.error("jina_api_exception", extra={"url": url, "error": str(e)}) # Prepare content for the LLM main_content_for_llm = { diff --git a/core/agent_core/rag/duckdb_api.py b/core/agent_core/rag/duckdb_api.py index bf51a10..6e4a665 100644 --- a/core/agent_core/rag/duckdb_api.py +++ b/core/agent_core/rag/duckdb_api.py @@ -41,8 +41,19 @@ def __init__(self, config: Dict[str, Any]): ) logger.info("duckdb_rag_store_initialized", extra={"database_name": os.path.basename(self.db_file), "embedding_model_id": emb_cfg.get('emb_model_id')}) - # Asynchronously initialize or check the database - asyncio.create_task(self._initialize_or_check_database()) + # Track initialization state - lazy initialization to avoid unawaited coroutine warnings + self._initialized = False + self._init_lock = asyncio.Lock() + + async def _ensure_initialized(self): + """Ensure the database is initialized before any operations. Thread-safe and idempotent.""" + if self._initialized: + return + async with self._init_lock: + if self._initialized: # Double-check after acquiring lock + return + await self._initialize_or_check_database() + self._initialized = True def _get_connection(self) -> duckdb.DuckDBPyConnection: """Synchronously gets a database connection.""" @@ -137,6 +148,7 @@ def _sync_check(): async def add_text_chunk(self, chunk_text: str, project_id: str, doc_id: str = None, url: str = None, meta: str = None, tags: list = None) -> Optional[int]: """Asynchronously adds a new text chunk to the metadata table.""" + await self._ensure_initialized() if not chunk_text: raise ValueError("chunk_text cannot be empty.") if not self.config.get('database_writable', False): @@ -166,6 +178,7 @@ def _sync_add(): async def process_pending_embeddings(self, batch_size: int = 50): """Asynchronously generates and stores embeddings for pending text chunks.""" + await self._ensure_initialized() if not self.config.get('database_writable', False): raise PermissionError(f"Data source '{self.config.get('source_name')}' is read-only, 'process_pending_embeddings' operation is not allowed.") @@ -216,6 +229,7 @@ def _sync_insert_embeddings(): async def vector_search_text(self, query_text: str, project_id: str, top_k: int = 5, tags: Optional[List[str]] = None) -> List[Dict]: """Asynchronously performs a vector search across multiple embedding columns (available for all sources).""" + await self._ensure_initialized() if not query_text or not project_id: return [] is_global_source = self.config.get('is_global', False) diff --git a/core/agent_core/services/jina_api.py b/core/agent_core/services/jina_api.py index e080e5d..19de2f6 100644 --- a/core/agent_core/services/jina_api.py +++ b/core/agent_core/services/jina_api.py @@ -17,15 +17,15 @@ def get_jina_key(): logger.error("JINA_KEY environment variable is not set") return jina_key -def test_jina_search(query="PocketFlow"): +def check_jina_search(query="PocketFlow"): """ - Test the Jina Search API connection. + Check the Jina Search API connection. Args: query (str, optional): The test search query. Defaults to "PocketFlow". Returns: - bool: Whether the connection test was successful. + bool: Whether the connection check was successful. """ jina_key = get_jina_key() if not jina_key: @@ -51,15 +51,15 @@ def test_jina_search(query="PocketFlow"): logger.error("jina_search_api_test_error", extra={"error_message": str(e)}) return False -def test_jina_visit(url="github.com"): +def check_jina_visit(url="github.com"): """ - Test the Jina URL Visit API connection. + Check the Jina URL Visit API connection. Args: url (str, optional): The test URL to visit. Defaults to "github.com". Returns: - bool: Whether the connection test was successful. + bool: Whether the connection check was successful. """ jina_key = get_jina_key() if not jina_key: @@ -92,5 +92,5 @@ def test_jina_visit(url="github.com"): ) # Test Jina API - print("Testing Jina Search API:", "Success" if test_jina_search() else "Failure") - print("Testing Jina Visit API:", "Success" if test_jina_visit() else "Failure") + print("Testing Jina Search API:", "Success" if check_jina_search() else "Failure") + print("Testing Jina Visit API:", "Success" if check_jina_visit() else "Failure") diff --git a/core/agent_profiles/llm_configs/associate_llm.yaml b/core/agent_profiles/llm_configs/associate_llm.yaml index 2f568c4..fa4b981 100644 --- a/core/agent_profiles/llm_configs/associate_llm.yaml +++ b/core/agent_profiles/llm_configs/associate_llm.yaml @@ -14,5 +14,8 @@ config: var: "AGENT_TEMPERATURE" default: 0.4 required: false + # Limit output tokens to prevent context overflow (input + max_tokens must be < 200K) + # Associates produce tool calls and summaries, not long documents - 16K is ample + max_tokens: 16384 max_retries: 2 wait_seconds_on_retry: 3 diff --git a/core/tests/test_context_admission_controller.py b/core/tests/test_context_admission_controller.py new file mode 100644 index 0000000..40a6f68 --- /dev/null +++ b/core/tests/test_context_admission_controller.py @@ -0,0 +1,423 @@ +""" +Unit tests for context_admission_controller module. + +Tests the pre-admission budget enforcement that prevents context spikes +by truncating oversized tool results before they enter the context. +""" + +import pytest +from unittest.mock import MagicMock, patch + +from agent_core.framework.context_admission_controller import ( + AdmissionDecision, + check_pre_admission, + calculate_admission_budget, + estimate_tokens, + estimate_result_tokens, + _score_and_sort_kb_items, + _serialize_payload, + ADMISSION_TARGET_UTILIZATION, + MIN_ADMISSION_TOKENS, + WARNING_THRESHOLD +) + + +class TestEstimateTokens: + """Tests for estimate_tokens helper function.""" + + def test_empty_string_returns_zero(self): + """Test empty string returns zero tokens.""" + assert estimate_tokens("") == 0 + + def test_counts_tokens_approximately(self): + """Test token counting gives reasonable estimates.""" + text = "This is a test sentence with several words." + count = estimate_tokens(text) + # ~44 chars / 4 = ~11 tokens + assert count > 5 + assert count < 20 + + def test_counts_long_text(self): + """Test counting works for longer text.""" + text = "word " * 1000 # 5000 chars + count = estimate_tokens(text) + # ~5000 chars / 4 = ~1250 tokens + assert count > 1000 + assert count < 1500 + + def test_minimum_one_token(self): + """Test that even single chars get at least 1 token.""" + assert estimate_tokens("a") >= 1 + + +class TestAdmissionDecision: + """Tests for AdmissionDecision dataclass.""" + + def test_decision_creation_full_admission(self): + """Test creating a decision for full admission.""" + decision = AdmissionDecision( + admit_full=True, + admitted_content={"payload": "test"}, + original_tokens=5000, + admitted_tokens=5000, + post_admission_utilization=0.225 + ) + + assert decision.admit_full is True + assert decision.deferred_content is None + assert decision.original_tokens == 5000 + + def test_decision_creation_truncated(self): + """Test creating a decision with truncation.""" + decision = AdmissionDecision( + admit_full=False, + admitted_content={"payload": "truncated"}, + deferred_content=[{"content": "deferred item"}], + deferred_kb_tokens=["<#CGKB-DEFERRED-0>"], + truncation_notice="⚠️ Items were deferred", + original_tokens=100000, + admitted_tokens=20000, + deferred_tokens=80000, + post_admission_utilization=0.40 + ) + + assert decision.admit_full is False + assert decision.admitted_tokens == 20000 + assert len(decision.deferred_kb_tokens) == 1 + + +class TestCalculateAdmissionBudget: + """Tests for calculate_admission_budget function.""" + + def test_budget_from_low_utilization(self): + """Test budget calculation when utilization is low.""" + # At 20K tokens (10% of 200K), should have (38% - 10%) = 28% available + budget = calculate_admission_budget( + current_tokens=20000, + context_limit=200000 + ) + # 38% of 200K = 76K target, minus 20K current = 56K available + assert budget == 56000 + + def test_budget_from_moderate_utilization(self): + """Test budget calculation at moderate utilization.""" + # At 60K tokens (30% of 200K), should have (38% - 30%) = 8% available + budget = calculate_admission_budget( + current_tokens=60000, + context_limit=200000 + ) + # 38% of 200K = 76K target, minus 60K current = 16K available + assert budget == 16000 + + def test_budget_near_target_returns_minimum(self): + """Test budget at target threshold returns minimum.""" + # At 74K tokens (37% of 200K), only 1% headroom = 2K + # Should return MIN_ADMISSION_TOKENS instead + budget = calculate_admission_budget( + current_tokens=74000, + context_limit=200000 + ) + # 38% of 200K = 76K target, minus 74K = 2K, but min is 5K + assert budget == MIN_ADMISSION_TOKENS + + def test_budget_over_target_returns_minimum(self): + """Test budget when already over target.""" + # At 100K tokens (50% of 200K), already over 38% target + budget = calculate_admission_budget( + current_tokens=100000, + context_limit=200000 + ) + # Should still get minimum + assert budget == MIN_ADMISSION_TOKENS + + def test_budget_with_custom_target(self): + """Test budget calculation with custom target utilization.""" + budget = calculate_admission_budget( + current_tokens=40000, # 20% of 200K + context_limit=200000, + target_utilization=0.50 # Higher target (50%) + ) + # 50% of 200K = 100K target, minus 40K current = 60K + assert budget == 60000 + + +class TestCheckPreAdmission: + """Tests for check_pre_admission function.""" + + def test_full_admission_small_result(self): + """Test that small results are fully admitted.""" + tool_result = { + "payload": {"result": "Small result"}, + "_knowledge_items_to_add": [] + } + + # 20K tokens = 10% utilization, plenty of room + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=20000, + model_name="anthropic/claude-sonnet-4-20250514" + ) + + assert decision.admit_full is True + assert decision.deferred_content is None + + def test_full_admission_with_kb_items_under_threshold(self): + """Test full admission when KB items fit within budget.""" + kb_items = [ + {"content": "Short content " * 50, "source_uri": "https://example.com"} + for _ in range(5) + ] + + tool_result = { + "payload": {"result": "Search completed"}, + "_knowledge_items_to_add": kb_items + } + + # Low utilization, small KB items should fit + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=10000, + model_name="anthropic/claude-sonnet-4-20250514" + ) + + assert decision.admit_full is True + assert len(decision.deferred_kb_tokens) == 0 + + def test_truncation_large_result(self): + """Test that large results are truncated.""" + # Create many KB items that would exceed budget + kb_items = [ + {"content": "Content block " * 500, "source_uri": f"https://example{i}.com"} + for i in range(50) + ] + + tool_result = { + "payload": {"result": "Search completed"}, + "_knowledge_items_to_add": kb_items + } + + # At 35% utilization, only 3% budget remaining (6K tokens) + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=70000, # 35% of 200K + model_name="anthropic/claude-sonnet-4-20250514" + ) + + assert decision.admit_full is False + assert decision.deferred_content is not None + assert len(decision.deferred_kb_tokens) > 0 + assert decision.admitted_tokens < decision.original_tokens + + @patch("agent_core.framework.context_admission_controller.get_model_context_limit") + def test_uses_model_context_limit(self, mock_get_limit): + """Test that model context limit is properly used.""" + mock_get_limit.return_value = 100000 # Smaller context + + tool_result = { + "payload": {"result": "Test"}, + "_knowledge_items_to_add": [] + } + + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=10000, + model_name="test-model" + ) + + mock_get_limit.assert_called_once() + # Post utilization should be based on 100K limit + assert decision.post_admission_utilization > 0 + + +class TestScoreAndSortKbItems: + """Tests for _score_and_sort_kb_items function.""" + + def test_sorts_by_source_authority(self): + """Test that items are sorted by source authority.""" + items = [ + {"content": "Regular content " * 100, "source_uri": "https://blog.example.com"}, + {"content": "Academic content " * 100, "source_uri": "https://example.edu/paper"}, + {"content": "Gov content " * 100, "source_uri": "https://example.gov/report"}, + ] + + sorted_items = _score_and_sort_kb_items(items) + + # Gov and edu sources should rank higher than blog + # Both .gov and .edu get +3.0, so either could be first based on position + top_sources = [sorted_items[0][0]["source_uri"], sorted_items[1][0]["source_uri"]] + assert any("gov" in s or "edu" in s for s in top_sources) + # Blog should be last (lowest authority) + assert "blog" in sorted_items[2][0]["source_uri"] + + def test_penalizes_very_short_content(self): + """Test that very short content is penalized.""" + items = [ + {"content": "Short", "source_uri": "https://example.com"}, + {"content": "Medium length content here " * 50, "source_uri": "https://example.com"}, + ] + + sorted_items = _score_and_sort_kb_items(items) + + # Medium content should rank higher than very short + assert len(sorted_items[0][0]["content"]) > len(sorted_items[1][0]["content"]) + + def test_handles_empty_items(self): + """Test handling empty item list.""" + sorted_items = _score_and_sort_kb_items([]) + assert sorted_items == [] + + def test_uses_relevance_metadata(self): + """Test that relevance metadata boosts score.""" + items = [ + {"content": "Content A " * 100, "source_uri": "https://example.com", "metadata": {}}, + {"content": "Content B " * 100, "source_uri": "https://example.com", "metadata": {"relevance_score": 0.95}}, + ] + + sorted_items = _score_and_sort_kb_items(items) + + # Item with relevance score should rank higher + assert sorted_items[0][0]["metadata"].get("relevance_score") == 0.95 + + +class TestSerializePayload: + """Tests for _serialize_payload function.""" + + def test_string_passthrough(self): + """Test that string payloads pass through unchanged.""" + result = _serialize_payload("test string") + assert result == "test string" + + def test_dict_serialization(self): + """Test that dicts are JSON serialized.""" + payload = {"key": "value", "number": 42} + result = _serialize_payload(payload) + assert "key" in result + assert "value" in result + + def test_none_returns_empty(self): + """Test that None returns empty string.""" + result = _serialize_payload(None) + assert result == "" + + +class TestEstimateResultTokens: + """Tests for estimate_result_tokens function.""" + + def test_counts_payload_tokens(self): + """Test counting payload tokens.""" + tool_result = { + "payload": {"result": "Test content " * 100}, + "_knowledge_items_to_add": [] + } + + tokens = estimate_result_tokens(tool_result) + assert tokens > 0 + + def test_counts_kb_items_tokens(self): + """Test counting KB items tokens.""" + tool_result = { + "payload": {}, + "_knowledge_items_to_add": [ + {"content": "KB content " * 100}, + {"content": "More KB content " * 100} + ] + } + + tokens = estimate_result_tokens(tool_result) + # Should count tokens from both KB items + assert tokens > estimate_tokens("KB content " * 100) + + def test_empty_result(self): + """Test empty result returns minimal tokens.""" + tool_result = { + "payload": {}, + "_knowledge_items_to_add": [] + } + + tokens = estimate_result_tokens(tool_result) + # Empty dict serializes to "{}", real tokenizer may count differently than heuristic + # Should still be small (under 20 tokens for empty payload) + assert tokens <= 20 + + +class TestIntegration: + """Integration tests for admission controller.""" + + def test_full_workflow_small_result(self): + """Test complete workflow for small result.""" + tool_result = { + "payload": {"instructional_prompt": "Here are the search results"}, + "_knowledge_items_to_add": [ + {"content": "Result content " * 50, "source_uri": "https://example.com"} + ] + } + + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=30000, # 15% utilization + model_name="anthropic/claude-sonnet-4-20250514" + ) + + assert decision.admit_full is True + assert decision.truncation_notice is None + assert decision.post_admission_utilization < WARNING_THRESHOLD + + def test_full_workflow_large_result_truncated(self): + """Test complete workflow for large result requiring truncation.""" + # Create large result that will exceed budget + kb_items = [ + { + "content": f"{'Content block ' * 300}\n" * 5, + "source_uri": f"https://example{i}.com", + "token": f"<#CGKB-{i:05d}>" + } + for i in range(30) + ] + + tool_result = { + "payload": {"instructional_prompt": "Search results"}, + "_knowledge_items_to_add": kb_items + } + + # At 35% utilization, only 3% budget remaining + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=70000, # 35% of 200K + model_name="anthropic/claude-sonnet-4-20250514" + ) + + assert decision.admit_full is False + assert decision.deferred_content is not None + assert len(decision.deferred_kb_tokens) > 0 + assert decision.truncation_notice is not None + assert "Context Budget Notice" in decision.truncation_notice + + def test_truncation_preserves_high_value_items(self): + """Test that truncation keeps high-value sources.""" + # Mix of sources with different authority + kb_items = [ + {"content": "Blog content " * 200, "source_uri": "https://blog.example.com"}, + {"content": "Academic content " * 200, "source_uri": "https://university.edu/paper"}, + {"content": "Gov content " * 200, "source_uri": "https://agency.gov/report"}, + {"content": "Medium content " * 200, "source_uri": "https://medium.com/article"}, + ] + + tool_result = { + "payload": {"result": "Done"}, + "_knowledge_items_to_add": kb_items + } + + # Force truncation with high utilization + decision = check_pre_admission( + tool_result=tool_result, + current_context_tokens=72000, # 36% - only 2% budget + model_name="anthropic/claude-sonnet-4-20250514" + ) + + # If truncation occurred, check that admitted content prioritizes authority + if not decision.admit_full: + admitted_items = decision.admitted_content.get("_knowledge_items_to_add", []) + if admitted_items: + # First admitted should be high-authority source + first_source = admitted_items[0].get("source_uri", "") + assert ".gov" in first_source or ".edu" in first_source diff --git a/core/tests/test_context_budget_guardian.py b/core/tests/test_context_budget_guardian.py index 298af8d..ad92fb5 100644 --- a/core/tests/test_context_budget_guardian.py +++ b/core/tests/test_context_budget_guardian.py @@ -32,20 +32,20 @@ def test_thresholds_are_ascending(self): assert WARNING_THRESHOLD < CRITICAL_THRESHOLD < EXCEEDED_THRESHOLD def test_thresholds_leave_headroom(self): - """EXCEEDED threshold should leave at least 30% headroom.""" - assert EXCEEDED_THRESHOLD <= 0.70 + """EXCEEDED threshold should leave at least 15% headroom.""" + assert EXCEEDED_THRESHOLD <= 0.85 def test_warning_threshold_value(self): - """WARNING should trigger at 40%.""" - assert WARNING_THRESHOLD == 0.40 + """WARNING should trigger at 60%.""" + assert WARNING_THRESHOLD == 0.60 def test_critical_threshold_value(self): - """CRITICAL should trigger at 55%.""" - assert CRITICAL_THRESHOLD == 0.55 + """CRITICAL should trigger at 75%.""" + assert CRITICAL_THRESHOLD == 0.75 def test_exceeded_threshold_value(self): - """EXCEEDED should trigger at 70%.""" - assert EXCEEDED_THRESHOLD == 0.70 + """EXCEEDED should trigger at 85%.""" + assert EXCEEDED_THRESHOLD == 0.85 class TestGetModelContextLimit: @@ -132,14 +132,14 @@ def test_basic_calculation(self): # Total available = 200000 - 30000 overhead = 170000 assert result["total_available"] == 170000 - # Summarization = 30% of 170000 = 51000 - assert result["summarization_budget"] == 51000 + # Summarization = 15% of 170000 = 25500 + assert result["summarization_budget"] == 25500 - # Worker total = 170000 - 51000 = 119000 - assert result["worker_budget_total"] == 119000 + # Worker total = 170000 - 25500 = 144500 + assert result["worker_budget_total"] == 144500 - # Per worker = 119000 / 3 = 39666 - assert result["per_worker_budget"] == 39666 + # Per worker = 144500 / 3 = 48166 + assert result["per_worker_budget"] == 48166 def test_single_worker(self): """Single worker should get full worker budget.""" @@ -207,36 +207,36 @@ def test_healthy_status(self): assert "Healthy" in metadata["recommendation"] def test_warning_status(self): - """40-55% should be WARNING.""" + """60-75% should be WARNING.""" status, metadata = assess_context_budget( - predicted_tokens=90000, # 45% of 200K + predicted_tokens=130000, # 65% of 200K model_name="anthropic/claude-sonnet-4" ) assert status == ContextBudgetStatus.WARNING - assert metadata["utilization_percent"] == 45.0 + assert metadata["utilization_percent"] == 65.0 assert "WARNING" in metadata["recommendation"] def test_critical_status(self): - """55-70% should be CRITICAL.""" + """75-85% should be CRITICAL.""" status, metadata = assess_context_budget( - predicted_tokens=120000, # 60% of 200K + predicted_tokens=160000, # 80% of 200K model_name="anthropic/claude-sonnet-4" ) assert status == ContextBudgetStatus.CRITICAL - assert metadata["utilization_percent"] == 60.0 + assert metadata["utilization_percent"] == 80.0 assert "CRITICAL" in metadata["recommendation"] def test_exceeded_status(self): - """Over 70% should be EXCEEDED.""" + """Over 85% should be EXCEEDED.""" status, metadata = assess_context_budget( - predicted_tokens=150000, # 75% of 200K + predicted_tokens=180000, # 90% of 200K model_name="anthropic/claude-sonnet-4" ) assert status == ContextBudgetStatus.EXCEEDED - assert metadata["utilization_percent"] == 75.0 + assert metadata["utilization_percent"] == 90.0 assert "EMERGENCY" in metadata["recommendation"] def test_metadata_includes_all_fields(self): @@ -265,18 +265,18 @@ def test_remaining_tokens_calculation(self): assert metadata["remaining_tokens"] == 150000 def test_boundary_at_warning_threshold(self): - """Exactly at 40% should be WARNING.""" + """Exactly at 60% should be WARNING.""" status, _ = assess_context_budget( - predicted_tokens=80000, # 40% of 200K + predicted_tokens=120000, # 60% of 200K model_name="anthropic/claude-sonnet-4" ) assert status == ContextBudgetStatus.WARNING def test_just_under_warning_threshold(self): - """Just under 40% should be HEALTHY.""" + """Just under 60% should be HEALTHY.""" status, _ = assess_context_budget( - predicted_tokens=79000, # 39.5% of 200K + predicted_tokens=119000, # 59.5% of 200K model_name="anthropic/claude-sonnet-4" ) diff --git a/core/tests/test_context_budget_handback.py b/core/tests/test_context_budget_handback.py new file mode 100644 index 0000000..c585087 --- /dev/null +++ b/core/tests/test_context_budget_handback.py @@ -0,0 +1,361 @@ +""" +Unit tests for context_budget_handback module. + +Tests the ContextBudgetHandback dataclass and helper functions for +packaging partial work when subagents exceed context budget. +""" + +import pytest +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +from agent_core.framework.context_budget_handback import ( + ContextBudgetHandback, + build_handback_from_context, + notify_principal_of_handback +) + + +class TestContextBudgetHandback: + """Tests for ContextBudgetHandback dataclass.""" + + def test_handback_creation_with_defaults(self): + """Test creating a handback with minimal required fields.""" + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="Associate_WebSearcher", + utilization_percent=95.5, + predicted_tokens=190000, + context_limit=200000 + ) + + assert handback.agent_id == "test_agent" + assert handback.module_id == "WM_1" + assert handback.utilization_percent == 95.5 + assert handback.kb_tokens == [] + assert handback.kb_token_count == 0 + assert handback.tool_calls_completed == [] + + def test_handback_creation_with_all_fields(self): + """Test creating a handback with all fields populated.""" + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="Associate_WebSearcher", + utilization_percent=95.5, + predicted_tokens=190000, + context_limit=200000, + kb_tokens=["<#CGKB-00001>", "<#CGKB-00002>"], + kb_token_count=2, + estimated_kb_content_tokens=5000, + tool_calls_completed=[{"tool": "web_search", "arguments_preview": "test query"}], + tool_calls_in_progress={"tool": "visit_url"}, + last_assistant_content_preview="Here are my findings...", + turns_completed=5, + start_timestamp="2025-12-30T10:00:00Z", + overflow_timestamp="2025-12-30T11:00:00Z" + ) + + assert handback.kb_token_count == 2 + assert len(handback.tool_calls_completed) == 1 + assert handback.turns_completed == 5 + + def test_to_dict(self): + """Test converting handback to dictionary.""" + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="Associate_WebSearcher", + utilization_percent=95.5, + predicted_tokens=190000, + context_limit=200000 + ) + + result = handback.to_dict() + + assert isinstance(result, dict) + assert result["agent_id"] == "test_agent" + assert result["utilization_percent"] == 95.5 + + def test_from_dict(self): + """Test creating handback from dictionary.""" + data = { + "agent_id": "test_agent", + "module_id": "WM_1", + "profile_name": "Associate_WebSearcher", + "utilization_percent": 95.5, + "predicted_tokens": 190000, + "context_limit": 200000, + "kb_tokens": ["<#CGKB-00001>"], + "kb_token_count": 1, + "estimated_kb_content_tokens": 2500, + "tool_calls_completed": [], + "tool_calls_in_progress": None, + "last_assistant_content_preview": "", + "turns_completed": 3, + "start_timestamp": "", + "overflow_timestamp": "" + } + + handback = ContextBudgetHandback.from_dict(data) + + assert handback.agent_id == "test_agent" + assert handback.kb_token_count == 1 + assert handback.turns_completed == 3 + + def test_get_principal_summary_prompt(self): + """Test generating summary prompt for Principal.""" + handback = ContextBudgetHandback( + agent_id="Assoc_WebSearche_4", + module_id="WM_4", + profile_name="Associate_WebSearcher_Academic", + utilization_percent=94.5, + predicted_tokens=189017, + context_limit=200000, + kb_tokens=["<#CGKB-00029>", "<#CGKB-00030>", "<#CGKB-00031>"], + kb_token_count=3, + estimated_kb_content_tokens=15000, + tool_calls_completed=[ + {"tool": "web_search", "arguments_preview": "palliative care GP"}, + {"tool": "visit_url", "arguments_preview": "https://example.com"} + ], + turns_completed=5 + ) + + prompt = handback.get_principal_summary_prompt() + + assert "Assoc_WebSearche_4" in prompt + assert "WM_4" in prompt + assert "94.5%" in prompt + assert "<#CGKB-00029>" in prompt + assert "web_search" in prompt + assert "Summarization Required" in prompt + + def test_get_principal_summary_prompt_truncates_long_kb_list(self): + """Test that KB token list is truncated if too long.""" + kb_tokens = [f"<#CGKB-{i:05d}>" for i in range(50)] + + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="test_profile", + utilization_percent=95.0, + predicted_tokens=190000, + context_limit=200000, + kb_tokens=kb_tokens, + kb_token_count=50 + ) + + prompt = handback.get_principal_summary_prompt() + + # Should show first 20 and indicate more + assert "<#CGKB-00000>" in prompt + assert "<#CGKB-00019>" in prompt + assert "+30 more" in prompt + + def test_get_deliverables_summary(self): + """Test generating deliverables summary string.""" + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="test_profile", + utilization_percent=95.0, + predicted_tokens=190000, + context_limit=200000, + kb_tokens=["<#CGKB-00001>", "<#CGKB-00002>"], + kb_token_count=2 + ) + + summary = handback.get_deliverables_summary() + + assert "CONTEXT BUDGET EXCEEDED" in summary + assert "test_agent" in summary + assert "2 KB items" in summary + assert "95.0%" in summary + + +class TestBuildHandbackFromContext: + """Tests for build_handback_from_context function.""" + + def test_build_handback_minimal_context(self): + """Test building handback from minimal context.""" + context = { + "state": { + "messages": [ + {"role": "user", "content": "Search for X"}, + {"role": "assistant", "content": "I will search for X"} + ], + "_context_budget": { + "utilization_percent": 95.0, + "context_limit": 200000 + } + }, + "meta": { + "agent_id": "test_agent", + "module_id": "WM_1" + }, + "refs": { + "run": { + "runtime": {} + } + } + } + + prep_res = { + "predicted_total_tokens": 190000 + } + + handback = build_handback_from_context( + agent_id="test_agent", + context=context, + prep_res=prep_res, + profile_name="test_profile" + ) + + assert handback.agent_id == "test_agent" + assert handback.utilization_percent == 95.0 + assert handback.turns_completed == 1 # One assistant message + + def test_build_handback_with_tool_calls(self): + """Test building handback captures tool call history.""" + context = { + "state": { + "messages": [ + {"role": "user", "content": "Search for X"}, + { + "role": "assistant", + "content": "I will search", + "tool_calls": [ + { + "id": "tc_1", + "function": {"name": "web_search", "arguments": '{"query": "test"}'} + } + ] + }, + {"role": "tool", "content": "Results..."}, + {"role": "assistant", "content": "Based on results..."} + ], + "_context_budget": { + "utilization_percent": 95.0, + "context_limit": 200000 + } + }, + "meta": {"agent_id": "test_agent"}, + "refs": {"run": {"runtime": {}}} + } + + prep_res = {"predicted_total_tokens": 190000} + + handback = build_handback_from_context( + agent_id="test_agent", + context=context, + prep_res=prep_res, + profile_name="test_profile" + ) + + assert len(handback.tool_calls_completed) == 1 + assert handback.tool_calls_completed[0]["tool"] == "web_search" + assert handback.turns_completed == 2 # Two assistant messages + + def test_build_handback_extracts_last_content(self): + """Test that last assistant content is captured.""" + context = { + "state": { + "messages": [ + {"role": "user", "content": "Search for X"}, + {"role": "assistant", "content": "First response"}, + {"role": "user", "content": "Continue"}, + {"role": "assistant", "content": "This is my latest analysis of the findings..."} + ], + "_context_budget": {"utilization_percent": 95.0} + }, + "meta": {"agent_id": "test_agent"}, + "refs": {"run": {"runtime": {}}} + } + + prep_res = {"predicted_total_tokens": 190000} + + handback = build_handback_from_context( + agent_id="test_agent", + context=context, + prep_res=prep_res, + profile_name="test_profile" + ) + + assert "latest analysis" in handback.last_assistant_content_preview + + +class TestNotifyPrincipalOfHandback: + """Tests for notify_principal_of_handback function.""" + + def test_notify_adds_to_inbox(self): + """Test that notification is added to Principal's inbox.""" + principal_context = { + "state": { + "inbox": [] + } + } + + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="test_profile", + utilization_percent=95.0, + predicted_tokens=190000, + context_limit=200000, + kb_tokens=["<#CGKB-00001>"], + kb_token_count=1 + ) + + notify_principal_of_handback(principal_context, handback) + + assert len(principal_context["state"]["inbox"]) == 1 + + notification = principal_context["state"]["inbox"][0] + assert notification["source"] == "CONTEXT_BUDGET_HANDBACK" + assert "test_agent" in notification["payload"]["message"] + assert notification["metadata"]["priority"] == "high" + + def test_notify_creates_inbox_if_missing(self): + """Test that inbox is created if not present.""" + principal_context = { + "state": {} + } + + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="test_profile", + utilization_percent=95.0, + predicted_tokens=190000, + context_limit=200000 + ) + + notify_principal_of_handback(principal_context, handback) + + assert "inbox" in principal_context["state"] + assert len(principal_context["state"]["inbox"]) == 1 + + def test_notify_includes_handback_data(self): + """Test that full handback data is included in notification.""" + principal_context = {"state": {"inbox": []}} + + handback = ContextBudgetHandback( + agent_id="test_agent", + module_id="WM_1", + profile_name="test_profile", + utilization_percent=95.0, + predicted_tokens=190000, + context_limit=200000, + kb_tokens=["<#CGKB-00001>", "<#CGKB-00002>"], + kb_token_count=2 + ) + + notify_principal_of_handback(principal_context, handback) + + notification = principal_context["state"]["inbox"][0] + handback_in_payload = notification["payload"]["handback"] + + assert handback_in_payload["agent_id"] == "test_agent" + assert handback_in_payload["kb_token_count"] == 2 diff --git a/core/tests/test_jina_api.py b/core/tests/test_jina_api.py index c94c70b..8b2fc95 100644 --- a/core/tests/test_jina_api.py +++ b/core/tests/test_jina_api.py @@ -6,8 +6,8 @@ Key functionality tested: - get_jina_key: Environment variable retrieval -- test_jina_search: Search API connectivity test -- test_jina_visit: URL visit API connectivity test +- check_jina_search: Search API connectivity check +- check_jina_visit: URL visit API connectivity check """ import pytest @@ -15,8 +15,8 @@ from unittest.mock import patch, MagicMock from agent_core.services.jina_api import ( get_jina_key, - test_jina_search, - test_jina_visit, + check_jina_search, + check_jina_visit, ) @@ -53,12 +53,12 @@ def test_returns_empty_string_if_set_empty(self): class TestJinaSearchAPI: - """Tests for test_jina_search function.""" + """Tests for check_jina_search function.""" def test_returns_false_when_no_api_key(self): """Test returns False when API key not available.""" with patch.dict(os.environ, {}, clear=True): - result = test_jina_search() + result = check_jina_search() assert result is False @@ -70,7 +70,7 @@ def test_returns_true_on_success(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): - result = test_jina_search() + result = check_jina_search() assert result is True mock_get.assert_called_once() @@ -83,7 +83,7 @@ def test_returns_false_on_non_200_status(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "invalid-key"}): - result = test_jina_search() + result = check_jina_search() assert result is False @@ -93,7 +93,7 @@ def test_returns_false_on_exception(self, mock_get): mock_get.side_effect = Exception("Network error") with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): - result = test_jina_search() + result = check_jina_search() assert result is False @@ -105,7 +105,7 @@ def test_uses_correct_url_format(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "key"}): - test_jina_search(query="test query") + check_jina_search(query="test query") call_url = mock_get.call_args[0][0] assert "s.jina.ai" in call_url @@ -119,7 +119,7 @@ def test_includes_auth_header(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "my-api-key"}): - test_jina_search() + check_jina_search() call_headers = mock_get.call_args[1]["headers"] assert "Authorization" in call_headers @@ -127,12 +127,12 @@ def test_includes_auth_header(self, mock_get): class TestJinaVisitAPI: - """Tests for test_jina_visit function.""" + """Tests for check_jina_visit function.""" def test_returns_false_when_no_api_key(self): """Test returns False when API key not available.""" with patch.dict(os.environ, {}, clear=True): - result = test_jina_visit() + result = check_jina_visit() assert result is False @@ -144,7 +144,7 @@ def test_returns_true_on_success(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): - result = test_jina_visit() + result = check_jina_visit() assert result is True @@ -156,7 +156,7 @@ def test_returns_false_on_non_200_status(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "key"}): - result = test_jina_visit() + result = check_jina_visit() assert result is False @@ -166,7 +166,7 @@ def test_returns_false_on_exception(self, mock_get): mock_get.side_effect = ConnectionError("Failed to connect") with patch.dict(os.environ, {"JINA_KEY": "valid-key"}): - result = test_jina_visit() + result = check_jina_visit() assert result is False @@ -178,7 +178,7 @@ def test_uses_correct_url_format(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "key"}): - test_jina_visit(url="example.com/page") + check_jina_visit(url="example.com/page") call_url = mock_get.call_args[0][0] assert "r.jina.ai" in call_url @@ -192,7 +192,7 @@ def test_includes_auth_header(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "secret-key"}): - test_jina_visit() + check_jina_visit() call_headers = mock_get.call_args[1]["headers"] assert "Authorization" in call_headers @@ -206,7 +206,7 @@ def test_default_url_is_github(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "key"}): - test_jina_visit() # No url parameter + check_jina_visit() # No url parameter call_url = mock_get.call_args[0][0] assert "github.com" in call_url @@ -223,7 +223,7 @@ def test_search_default_query(self, mock_get): mock_get.return_value = mock_response with patch.dict(os.environ, {"JINA_KEY": "key"}): - test_jina_search() # No query parameter + check_jina_search() # No query parameter call_url = mock_get.call_args[0][0] assert "PocketFlow" in call_url diff --git a/core/tests/test_turn_manager_orphan_detection.py b/core/tests/test_turn_manager_orphan_detection.py new file mode 100644 index 0000000..506adfe --- /dev/null +++ b/core/tests/test_turn_manager_orphan_detection.py @@ -0,0 +1,566 @@ +"""Tests for turn_manager orphan tool interaction detection.""" + +import pytest +from datetime import datetime, timezone, timedelta +from unittest.mock import MagicMock, patch + +from agent_core.framework.turn_manager import TurnManager, ORPHAN_TOOL_INTERACTION_TIMEOUT_SECONDS + + +class TestOrphanToolInteractionDetection: + """Tests for detecting and handling orphaned tool interactions. + + The TurnManager.detect_orphaned_tool_interactions method detects tool interactions + that have been in "running" state for longer than the timeout threshold. This helps + identify silent failures where tools crashed before sending results back. + """ + + @pytest.fixture + def turn_manager(self): + """Create a TurnManager instance for testing.""" + return TurnManager() + + @pytest.fixture + def team_state(self): + """Create a team_state dict for testing.""" + return {"turns": []} + + def _create_turn_with_tool_interaction( + self, + turn_id: str, + tool_name: str, + tool_status: str, + turn_status: str = "completed", + start_time: datetime = None, + end_time: datetime = None, + ) -> dict: + """Helper to create a turn dict with a tool interaction.""" + now = datetime.now(timezone.utc) + if start_time is None: + # Default to 1 hour ago (well past the timeout) + start_time = now - timedelta(hours=1) + if end_time is None and turn_status == "completed": + end_time = now - timedelta(minutes=30) + + return { + "turn_id": turn_id, + "status": turn_status, + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat() if end_time else None, + "tool_interactions": [ + { + "tool_call_id": f"tool_{turn_id}", + "tool_name": tool_name, + "status": tool_status, + "start_time": start_time.isoformat(), + "end_time": None if tool_status == "running" else (start_time + timedelta(seconds=10)).isoformat(), + } + ], + "outputs": {"next_action": tool_name}, + } + + def test_detect_orphans_finds_timed_out_running_tools( + self, turn_manager, team_state + ): + """Test that orphan detection finds tools stuck in 'running' state past timeout.""" + # Create a turn with a tool stuck in "running" for over an hour + orphan_turn = self._create_turn_with_tool_interaction( + turn_id="turn_Assoc_WebSearche_8_9954a710", + tool_name="visit_url", + tool_status="running", + turn_status="completed", + ) + team_state["turns"] = [orphan_turn] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 1 + turn_id, tool_call_id, tool_interaction = orphans[0] + assert turn_id == "turn_Assoc_WebSearche_8_9954a710" + assert tool_interaction["tool_name"] == "visit_url" + assert tool_interaction["status"] == "running" + + def test_detect_orphans_ignores_properly_completed_tools( + self, turn_manager, team_state + ): + """Test that properly completed tools are not flagged as orphans.""" + completed_turn = self._create_turn_with_tool_interaction( + turn_id="turn_Partner_12345", + tool_name="web_search", + tool_status="completed", + turn_status="completed", + ) + team_state["turns"] = [completed_turn] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 0 + + def test_detect_orphans_ignores_recently_started_tools( + self, turn_manager, team_state + ): + """Test that recently started running tools are not flagged (still within timeout).""" + # Tool started just now - should not be flagged + recent_start = datetime.now(timezone.utc) - timedelta(seconds=30) + running_turn = self._create_turn_with_tool_interaction( + turn_id="turn_Associate_active", + tool_name="visit_url", + tool_status="running", + turn_status="running", + start_time=recent_start, + end_time=None, + ) + running_turn["end_time"] = None + team_state["turns"] = [running_turn] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 0 + + def test_detect_orphans_finds_multiple_orphans( + self, turn_manager, team_state + ): + """Test detection of multiple orphaned tool interactions.""" + orphan1 = self._create_turn_with_tool_interaction( + turn_id="turn_Assoc_WebSearche_8_abc123", + tool_name="visit_url", + tool_status="running", + turn_status="completed", + ) + orphan2 = self._create_turn_with_tool_interaction( + turn_id="turn_Assoc_WebSearche_9_def456", + tool_name="web_search", + tool_status="running", + turn_status="completed", + ) + completed_turn = self._create_turn_with_tool_interaction( + turn_id="turn_Partner_normal", + tool_name="manage_work_modules", + tool_status="completed", + turn_status="completed", + ) + team_state["turns"] = [orphan1, orphan2, completed_turn] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 2 + orphan_turn_ids = {o[0] for o in orphans} + assert "turn_Assoc_WebSearche_8_abc123" in orphan_turn_ids + assert "turn_Assoc_WebSearche_9_def456" in orphan_turn_ids + + def test_detect_orphans_handles_empty_turns( + self, turn_manager, team_state + ): + """Test that empty turns list doesn't cause errors.""" + team_state["turns"] = [] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 0 + + def test_detect_orphans_handles_turns_without_tool_interactions( + self, turn_manager, team_state + ): + """Test turns without tool_interactions field are handled gracefully.""" + turn_no_tools = { + "turn_id": "turn_user_message", + "status": "completed", + "start_time": datetime.now(timezone.utc).isoformat(), + "end_time": datetime.now(timezone.utc).isoformat(), + # No tool_interactions field + } + team_state["turns"] = [turn_no_tools] + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 0 + + def test_detect_orphans_handles_missing_turns_key(self, turn_manager): + """Test that missing turns key is handled gracefully.""" + team_state = {} # No turns key + + orphans = turn_manager.detect_orphaned_tool_interactions(team_state) + + assert len(orphans) == 0 + + def test_detect_orphans_respects_custom_timeout( + self, turn_manager, team_state + ): + """Test that custom timeout parameter is respected.""" + # Tool started 10 minutes ago + start_time = datetime.now(timezone.utc) - timedelta(minutes=10) + running_turn = self._create_turn_with_tool_interaction( + turn_id="turn_test", + tool_name="visit_url", + tool_status="running", + turn_status="completed", + start_time=start_time, + ) + team_state["turns"] = [running_turn] + + # With 5 minute timeout (default), should be detected + orphans = turn_manager.detect_orphaned_tool_interactions(team_state, timeout_seconds=300) + assert len(orphans) == 1 + + # With 15 minute timeout, should NOT be detected + orphans = turn_manager.detect_orphaned_tool_interactions(team_state, timeout_seconds=900) + assert len(orphans) == 0 + + +class TestOrphanToolInteractionRecovery: + """Tests for recovering from orphaned tool interactions via finalize_orphaned_tool_interactions.""" + + @pytest.fixture + def turn_manager(self): + """Create a TurnManager instance for testing.""" + return TurnManager() + + @pytest.fixture + def team_state(self): + """Create a team_state dict for testing.""" + return {"turns": []} + + def test_finalize_orphaned_tools_marks_them_as_error(self, turn_manager, team_state): + """Test that orphaned tools are marked as error for recovery.""" + now = datetime.now(timezone.utc) + orphan_turn = { + "turn_id": "turn_Assoc_WebSearche_8_orphan", + "status": "completed", + "start_time": (now - timedelta(hours=1)).isoformat(), + "end_time": (now - timedelta(minutes=30)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_orphan_1", + "tool_name": "visit_url", + "status": "running", + "start_time": (now - timedelta(hours=1)).isoformat(), + "end_time": None, + } + ], + "outputs": {"next_action": "visit_url"}, + } + team_state["turns"] = [orphan_turn] + + # Finalize orphans (marks them as error) + finalized_count = turn_manager.finalize_orphaned_tool_interactions(team_state) + + assert finalized_count == 1 + # Verify the tool interaction was updated + updated_turn = team_state["turns"][0] + assert updated_turn["tool_interactions"][0]["status"] == "error" + assert "error_details" in updated_turn["tool_interactions"][0] + assert "timed out" in updated_turn["tool_interactions"][0]["error_details"].lower() + + def test_finalize_preserves_completed_tools( + self, turn_manager, team_state + ): + """Test that finalizing orphans doesn't affect properly completed tools.""" + now = datetime.now(timezone.utc) + completed_turn = { + "turn_id": "turn_normal", + "status": "completed", + "start_time": (now - timedelta(hours=1)).isoformat(), + "end_time": (now - timedelta(minutes=30)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_ok", + "tool_name": "web_search", + "status": "completed", + "start_time": (now - timedelta(hours=1)).isoformat(), + "end_time": (now - timedelta(minutes=45)).isoformat(), + } + ], + "outputs": {"next_action": "web_search"}, + } + team_state["turns"] = [completed_turn] + + finalized_count = turn_manager.finalize_orphaned_tool_interactions(team_state) + + assert finalized_count == 0 + assert team_state["turns"][0]["tool_interactions"][0]["status"] == "completed" + + def test_finalize_multiple_orphans(self, turn_manager, team_state): + """Test finalizing multiple orphaned tool interactions.""" + now = datetime.now(timezone.utc) + orphan1 = { + "turn_id": "turn_orphan_1", + "status": "completed", + "start_time": (now - timedelta(hours=2)).isoformat(), + "end_time": (now - timedelta(hours=1)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_1", + "tool_name": "visit_url", + "status": "running", + "start_time": (now - timedelta(hours=2)).isoformat(), + "end_time": None, + } + ], + } + orphan2 = { + "turn_id": "turn_orphan_2", + "status": "completed", + "start_time": (now - timedelta(hours=2)).isoformat(), + "end_time": (now - timedelta(hours=1)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_2", + "tool_name": "web_search", + "status": "running", + "start_time": (now - timedelta(hours=2)).isoformat(), + "end_time": None, + } + ], + } + team_state["turns"] = [orphan1, orphan2] + + finalized_count = turn_manager.finalize_orphaned_tool_interactions(team_state) + + assert finalized_count == 2 + assert team_state["turns"][0]["tool_interactions"][0]["status"] == "error" + assert team_state["turns"][1]["tool_interactions"][0]["status"] == "error" + + +class TestDispatchHistoryAnomalyDetection: + """Tests for detecting dispatch history anomalies.""" + + @pytest.fixture + def mock_shared_state(self): + """Create a mock shared state with dispatch_history.""" + state = MagicMock() + state.team_state = { + "dispatch_history": [], + "work_modules": {}, + } + return state + + def test_detect_stale_running_dispatches(self, mock_shared_state): + """Test detection of dispatches stuck in RUNNING state.""" + from agent_core.nodes.custom_nodes.dispatcher_node import detect_dispatch_anomalies + + now = datetime.now(timezone.utc) + stale_dispatch = { + "dispatch_id": "Assoc_WebSearche_8", + "module_id": "WM_8", + "status": "RUNNING", + "start_timestamp": (now - timedelta(hours=3)).isoformat(), + "end_timestamp": None, + "error_details": None, + } + mock_shared_state.team_state["dispatch_history"] = [stale_dispatch] + mock_shared_state.team_state["work_modules"] = { + "WM_8": {"status": "ongoing", "sub_context_id": None} + } + + anomalies = detect_dispatch_anomalies( + mock_shared_state, + stale_threshold_minutes=60 + ) + + assert len(anomalies) == 1 + assert anomalies[0]["dispatch_id"] == "Assoc_WebSearche_8" + assert anomalies[0]["anomaly_type"] == "stale_running" + + def test_detect_dispatches_without_subcontext(self, mock_shared_state): + """Test detection of dispatches that completed but have no sub_context.""" + from agent_core.nodes.custom_nodes.dispatcher_node import detect_dispatch_anomalies + + now = datetime.now(timezone.utc) + dispatch = { + "dispatch_id": "Assoc_WebSearche_9", + "module_id": "WM_9", + "status": "RUNNING", + "start_timestamp": (now - timedelta(minutes=30)).isoformat(), + "end_timestamp": None, + "error_details": None, + } + mock_shared_state.team_state["dispatch_history"] = [dispatch] + mock_shared_state.team_state["work_modules"] = { + "WM_9": {"status": "ongoing", "sub_context_id": None} + } + + anomalies = detect_dispatch_anomalies( + mock_shared_state, + stale_threshold_minutes=15 # Lower threshold + ) + + assert len(anomalies) == 1 + assert anomalies[0]["module_id"] == "WM_9" + assert "no sub_context" in anomalies[0].get("details", "").lower() or anomalies[0]["anomaly_type"] == "stale_running" + + def test_no_anomalies_for_completed_dispatches(self, mock_shared_state): + """Test that properly completed dispatches don't trigger anomalies.""" + from agent_core.nodes.custom_nodes.dispatcher_node import detect_dispatch_anomalies + + now = datetime.now(timezone.utc) + completed_dispatch = { + "dispatch_id": "Assoc_Analyst_1", + "module_id": "WM_1", + "status": "COMPLETED", + "start_timestamp": (now - timedelta(hours=2)).isoformat(), + "end_timestamp": (now - timedelta(hours=1)).isoformat(), + "final_summary": "Task completed successfully", + "error_details": None, + } + mock_shared_state.team_state["dispatch_history"] = [completed_dispatch] + mock_shared_state.team_state["work_modules"] = { + "WM_1": {"status": "completed", "sub_context_id": "ctx_wm1"} + } + + anomalies = detect_dispatch_anomalies(mock_shared_state) + + assert len(anomalies) == 0 + + +class TestIntegrationOrphanDetection: + """Integration tests for orphan detection in realistic scenarios.""" + + @pytest.fixture + def realistic_team_state(self): + """Create a realistic team_state dict mimicking the stuck session.""" + now = datetime.now(timezone.utc) + return { + "turns": [ + # Normal completed Partner turn + { + "turn_id": "turn_Partner_normal", + "status": "completed", + "start_time": (now - timedelta(hours=5)).isoformat(), + "end_time": (now - timedelta(hours=4, minutes=55)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_1", + "tool_name": "manage_work_modules", + "status": "completed", + "start_time": (now - timedelta(hours=5)).isoformat(), + "end_time": (now - timedelta(hours=4, minutes=58)).isoformat(), + } + ], + "outputs": {"next_action": "manage_work_modules"}, + }, + # Principal dispatch turn with incomplete tool + { + "turn_id": "turn_Principal_1aeaca64", + "status": "completed", + "start_time": (now - timedelta(hours=4)).isoformat(), + "end_time": (now - timedelta(hours=3, minutes=59)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "toolu_0167AXjWtbDweEr5pXYovW3o", + "tool_name": "dispatch_submodules", + "status": "running", # ORPHANED! + "start_time": (now - timedelta(hours=4)).isoformat(), + "end_time": None, + } + ], + "outputs": {"next_action": "dispatch_submodules"}, + }, + # WM_8 Associate turn with orphaned tool + { + "turn_id": "turn_Assoc_WebSearche_8_9954a710", + "status": "completed", + "start_time": (now - timedelta(hours=3, minutes=30)).isoformat(), + "end_time": (now - timedelta(hours=3, minutes=20)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_wm8", + "tool_name": "visit_url", + "status": "running", # ORPHANED! + "start_time": (now - timedelta(hours=3, minutes=30)).isoformat(), + "end_time": None, + } + ], + "outputs": {"next_action": "visit_url"}, + }, + # WM_9 Associate turn with orphaned tool + { + "turn_id": "turn_Assoc_WebSearche_9_dd68373e", + "status": "completed", + "start_time": (now - timedelta(hours=3, minutes=25)).isoformat(), + "end_time": (now - timedelta(hours=3, minutes=15)).isoformat(), + "tool_interactions": [ + { + "tool_call_id": "tool_wm9", + "tool_name": "visit_url", + "status": "running", # ORPHANED! + "start_time": (now - timedelta(hours=3, minutes=25)).isoformat(), + "end_time": None, + } + ], + "outputs": {"next_action": "visit_url"}, + }, + ], + "dispatch_history": [ + { + "dispatch_id": "Assoc_WebSearche_8", + "module_id": "WM_8", + "status": "RUNNING", + "start_timestamp": (now - timedelta(hours=4)).isoformat(), + "end_timestamp": None, + }, + { + "dispatch_id": "Assoc_WebSearche_9", + "module_id": "WM_9", + "status": "RUNNING", + "start_timestamp": (now - timedelta(hours=4)).isoformat(), + "end_timestamp": None, + }, + ], + "work_modules": { + "WM_8": {"status": "ongoing", "sub_context_id": None}, + "WM_9": {"status": "ongoing", "sub_context_id": None}, + }, + "is_principal_flow_running": False, + } + + def test_full_orphan_detection_on_stuck_session(self, realistic_team_state): + """Test that all orphans are detected in a realistic stuck session.""" + turn_manager = TurnManager() + + orphans = turn_manager.detect_orphaned_tool_interactions(realistic_team_state) + + # Should find 3 orphans: dispatch_submodules, visit_url (WM_8), visit_url (WM_9) + assert len(orphans) == 3 + + # Orphans are tuples of (turn_id, tool_call_id, tool_interaction) + orphan_tools = {o[2]["tool_name"] for o in orphans} + assert "dispatch_submodules" in orphan_tools + assert "visit_url" in orphan_tools + + orphan_turn_ids = {o[0] for o in orphans} + assert "turn_Principal_1aeaca64" in orphan_turn_ids + assert "turn_Assoc_WebSearche_8_9954a710" in orphan_turn_ids + assert "turn_Assoc_WebSearche_9_dd68373e" in orphan_turn_ids + + def test_dispatch_anomaly_detection_on_stuck_session(self, realistic_team_state): + """Test dispatch anomaly detection on realistic stuck session.""" + from agent_core.nodes.custom_nodes.dispatcher_node import detect_dispatch_anomalies + + # detect_dispatch_anomalies expects shared_state with team_state attribute + # or a dict with 'team_state' key + mock_shared = MagicMock() + mock_shared.team_state = realistic_team_state + + anomalies = detect_dispatch_anomalies( + mock_shared, + stale_threshold_minutes=60 + ) + + # Should find 2 stale dispatches (WM_8 and WM_9) + assert len(anomalies) == 2 + + anomaly_modules = {a["module_id"] for a in anomalies} + assert "WM_8" in anomaly_modules + assert "WM_9" in anomaly_modules + + def test_recovery_marks_all_orphans_as_error(self, realistic_team_state): + """Test that recovery process marks all orphaned tools as error.""" + turn_manager = TurnManager() + + finalized_count = turn_manager.finalize_orphaned_tool_interactions(realistic_team_state) + + assert finalized_count == 3 + + # Verify all orphans are now marked as error + orphans_after = turn_manager.detect_orphaned_tool_interactions(realistic_team_state) + assert len(orphans_after) == 0 diff --git a/docs/architecture/context-budget-management.md b/docs/architecture/context-budget-management.md index 450f08d..3d15a1d 100644 --- a/docs/architecture/context-budget-management.md +++ b/docs/architecture/context-budget-management.md @@ -121,7 +121,7 @@ def work_modules_ingestor(payload: Any, params: Dict, context: Dict) -> str: **File**: `core/agent_core/framework/context_budget_guardian.py` -The guardian already supports 1M context detection. Key behaviors: +The guardian supports 1M context detection and provides agent-type-aware handling. Key behaviors: 1. **Priority Resolution**: - First: Check explicit `max_context_tokens` in config @@ -129,10 +129,29 @@ The guardian already supports 1M context detection. Key behaviors: - Third: Model family defaults - Fourth: Conservative 100K default -2. **Thresholds** (already implemented): - - WARNING: 40% - Start suggesting wrap-up - - CRITICAL: 55% - Force completion - - EXCEEDED: 70% - Circuit breaker, 30% remains for wrap-up +2. **Thresholds**: + - HEALTHY: <60% - Normal operation + - WARNING: 60-75% - Inject guidance directive to wrap up + - CRITICAL: 75-85% - Force completion (for agents with flow-ending tools) + - EXCEEDED: >85% - Circuit breaker fires, 15% remains for wrap-up + +3. **Agent-Type-Aware Behavior**: + + | Agent Type | WARNING | CRITICAL | EXCEEDED | + |------------|---------|----------|----------| + | **Principal** | Directive: "call `finish_flow`" | Forces `finish_flow` | Circuit breaker β†’ `finish_flow` + synthesis | + | **Partner** | Directive: "wrap up response" | Guidance only (no forcing) | Circuit breaker β†’ user message | + | **Associate** | Directive: "call `generate_message_summary`" | Forces `generate_message_summary` | Circuit breaker β†’ handback for Principal | + + **Key Design Insight**: Partner agents do NOT have `finish_flow` or `generate_message_summary` in their toolset, so: + - They receive increasingly urgent guidance directives at WARNING/CRITICAL + - They CANNOT be forced to call a non-existent tool + - At EXCEEDED, they return a user-visible message explaining the limit was reached + +4. **Handback Behavior** (Associates only): + - When an Associate hits EXCEEDED, it packages its collected work (KB tokens, tool history, partial findings) into a `ContextBudgetHandback` structure + - This handback is stored in `deliverables._handback` for Principal to access + - Principal can expand KB tokens and summarize the partial work itself ### 2.1 Provider-Aware Token Counting @@ -244,7 +263,7 @@ Add to `fim_protocol`: ```yaml fim_protocol: trigger_conditions: - budget_threshold_percent: 70 + budget_threshold_percent: 75 # Aligns with CRITICAL threshold max_turns_without_fim: 5 mandatory_deliverable_check: @@ -321,11 +340,12 @@ ANTHROPIC_EXTRA_HEADERS={"anthropic-beta": "context-1m-2025-08-07"} ## Implementation Priority -1. **[CRITICAL] Fix work_modules_ingestor** - Immediate token reduction +1. **[CRITICAL] Fix work_modules_ingestor** - Immediate token reduction (DONE βœ“) 2. **[HIGH] Verify 1M context** - Safety net (DONE βœ“) -3. **[HIGH] Graceful circuit breaker** - Better failure recovery -4. **[MEDIUM] Deliverable enforcement** - Data quality -5. **[LOW] Tool docstring optimization** - Long-term token savings +3. **[HIGH] Graceful circuit breaker** - Better failure recovery (DONE βœ“) +4. **[HIGH] Agent-type-aware handling** - Respect Partner/Principal/Associate capabilities (DONE βœ“) +5. **[MEDIUM] Deliverable enforcement** - Data quality (fim_protocol not yet implemented) +6. **[LOW] Tool docstring optimization** - Long-term token savings ## Monitoring & Alerts @@ -333,21 +353,22 @@ The system should log warnings when: 1. Single work module exceeds 10K tokens when serialized 2. Total work_modules payload exceeds 50K tokens -3. Context utilization exceeds WARNING threshold (40%) +3. Context utilization exceeds WARNING threshold (60%) 4. Any agent completes with 0 deliverables 5. Circuit breaker fires (should be exceptional, not routine) +6. Partner agent reaches EXCEEDED without graceful completion ## Testing Checklist -- [ ] Verify 1M context enabled: `extra_headers` resolved correctly -- [ ] Verify `max_context_tokens: 1000000` in resolved config -- [ ] Verify work_modules_ingestor excludes context_archive -- [ ] Test circuit breaker synthesizes partial results -- [ ] Confirm deliverable capture enforcement works +- [x] Verify 1M context enabled: `extra_headers` resolved correctly *(test_context_budget_guardian.py)* +- [x] Verify `max_context_tokens: 1000000` in resolved config *(test_context_budget_guardian.py)* +- [x] Verify work_modules_ingestor excludes context_archive *(test_ingestors.py)* +- [x] Test circuit breaker synthesizes partial results *(implemented in context_budget_guardian.py)* +- [ ] Confirm deliverable capture enforcement works *(fim_protocol not yet implemented)* - [ ] Load test with complex multi-module scenario -- [ ] Verify inheritance budget computation works correctly -- [ ] Test content selection uses LLM summary when available -- [ ] Confirm hydration occurs before selection (not after) +- [x] Verify inheritance budget computation works correctly *(test_content_selection.py)* +- [x] Test content selection uses LLM summary when available *(test_content_selection.py)* +- [x] Confirm hydration occurs before selection (not after) *(test_content_selection.py)* --- diff --git a/frontend/app/chat/lib/flow-utils.ts b/frontend/app/chat/lib/flow-utils.ts index 7ce7927..1e1b311 100644 --- a/frontend/app/chat/lib/flow-utils.ts +++ b/frontend/app/chat/lib/flow-utils.ts @@ -4,12 +4,12 @@ import { FlowNodeData } from '@/app/stores/sessionStore'; // Define fallback dimensions for the initial render pass. // These are used before the actual node sizes are measured. -// Updated to fixed height levels. +// Updated to realistic heights that account for header + content + tools + footer. export const NODE_FALLBACK_DIMENSIONS = { - turn: { width: 340, height: 220 }, // Base height: header + content(S) + tools + footer - principal: { width: 340, height: 180 }, // Principal node is slightly smaller - agent: { width: 320, height: 160 }, // Agent node - default: { width: 280, height: 100 }, + turn: { width: 340, height: 320 }, // Base height: header(40) + content(L=160) + tools(80) + footer(20) + padding + principal: { width: 340, height: 280 }, // Principal node + agent: { width: 320, height: 260 }, // Agent node + default: { width: 280, height: 150 }, gather: { width: 340, height: 35 }, // Gather node aligned with other cards' width }; @@ -211,8 +211,8 @@ export const getLayoutedElements = ( }); // Layout configuration - const LEVEL_SPACING = 50; // Gap between levels (in pixels) - const MIN_NODE_HEIGHT = 60; // Minimum node height for consistent spacing + const LEVEL_SPACING = 80; // Gap between levels (in pixels) - increased for better separation + const MIN_NODE_HEIGHT = 200; // Minimum node height for consistent spacing - accounts for content boxes const HORIZONTAL_SPACING = 40; // Spacing between node edges (not centers) const VIEWPORT_CENTER_X = 500; // Center X coordinate for viewport diff --git a/frontend/app/stores/sessionStore.ts b/frontend/app/stores/sessionStore.ts index b749547..5a7a19c 100644 --- a/frontend/app/stores/sessionStore.ts +++ b/frontend/app/stores/sessionStore.ts @@ -3,8 +3,7 @@ import { v4 as uuidv4 } from 'uuid'; import { CSSProperties } from 'react'; import { selectionStore } from './selectionStore'; // Import selectionStore import { config } from '@/app/config'; -import { ProjectService } from '@/lib/api'; -import { getSessionManager, SessionManager, SessionTokens } from '@/lib/sessionManager'; +import { getSessionManager, SessionManager } from '@/lib/sessionManager'; import type { Turn as OriginalTurn, ToolInteraction } from '@/app/chat/types/conversation'; // <-- New import // Define the shape of `llm_interaction` as we expect it, including `actual_usage`. diff --git a/frontend/lib/sessionManager.ts b/frontend/lib/sessionManager.ts index 2f07d95..ee99922 100644 --- a/frontend/lib/sessionManager.ts +++ b/frontend/lib/sessionManager.ts @@ -486,87 +486,51 @@ export class SessionManager { /** * Get or create a session, checking for reconnection possibilities. + * + * IMPORTANT: We always create a NEW session_id for WebSocket connection, + * because the backend removes session_ids from pending_websocket_sessions + * after the first WebSocket connection. But we preserve runId/lastEventId + * from the old session for reconnection purposes. */ async getOrCreateSession(projectId: string = 'default'): Promise<{ tokens: SessionTokens; isReconnect: boolean; reconnectInfo?: { runId: string; lastEventId?: number }; }> { - // Check for existing session + // Check for existing session to get reconnection info const existingSession = this.loadSession(); + let reconnectInfo: { runId: string; lastEventId?: number } | undefined; - if (existingSession) { - // Check if JWT is still valid - const isExpired = Date.now() >= existingSession.expiresAt - 5000; - - if (isExpired) { - // Try to refresh - const refreshed = await this.performSilentRefresh(); - if (!refreshed) { - // Create new session - const tokens = await this.createSession(projectId); - return { tokens, isReconnect: false }; - } - - // Reload refreshed session - const refreshedSession = this.loadSession()!; - const tokens: SessionTokens = { - session_id: refreshedSession.sessionId, - jwt_token: refreshedSession.jwtToken, - refresh_token: refreshedSession.refreshToken, - expires_in: Math.floor((refreshedSession.expiresAt - Date.now()) / 1000), - }; - - // Check for reconnection - if (refreshedSession.runId) { - const { canReconnect } = await this.checkForReconnection(); - if (canReconnect) { - return { - tokens, - isReconnect: true, - reconnectInfo: { - runId: refreshedSession.runId, - lastEventId: refreshedSession.lastEventId, - }, - }; - } - } - - return { tokens, isReconnect: false }; - } - - // JWT is still valid - const tokens: SessionTokens = { - session_id: existingSession.sessionId, - jwt_token: existingSession.jwtToken, - refresh_token: existingSession.refreshToken, - expires_in: Math.floor((existingSession.expiresAt - Date.now()) / 1000), - }; - - // Schedule refresh if not already scheduled - this.scheduleAutoRefresh(tokens); - - // Check for reconnection - if (existingSession.runId) { + if (existingSession?.runId) { + // Check if we can reconnect to the existing run + try { const { canReconnect } = await this.checkForReconnection(); if (canReconnect) { - return { - tokens, - isReconnect: true, - reconnectInfo: { - runId: existingSession.runId, - lastEventId: existingSession.lastEventId, - }, + reconnectInfo = { + runId: existingSession.runId, + lastEventId: existingSession.lastEventId, }; + console.log('[SessionManager] Found reconnectable run:', reconnectInfo); } + } catch (e) { + console.warn('[SessionManager] Failed to check reconnection:', e); } - - return { tokens, isReconnect: false }; } - // No existing session - create new + // Always create a new session for WebSocket connection + // The backend requires a fresh session_id in pending_websocket_sessions const tokens = await this.createSession(projectId); - return { tokens, isReconnect: false }; + + // Preserve run info in the new session for reconnection + if (reconnectInfo) { + this.updateRunInfo(reconnectInfo.runId, reconnectInfo.lastEventId); + } + + return { + tokens, + isReconnect: !!reconnectInfo, + reconnectInfo, + }; } } diff --git a/scripts/analyze_session.py b/scripts/analyze_session.py index 1939aad..2f3b2a6 100755 --- a/scripts/analyze_session.py +++ b/scripts/analyze_session.py @@ -9,9 +9,10 @@ - Token utilization and budget compliance - Tool usage patterns - Error detection and diagnosis +- Thrashing root cause analysis Usage: - python analyze_session.py [--level LEVEL] [--focus FOCUS] + python analyze_session.py [--mode MODE] [--agent AGENT] Input: Can be either: @@ -19,27 +20,37 @@ - A session URL: http://localhost:3800/webview/r?id=tentacled-pearl-oriole - Just a session ID: tentacled-pearl-oriole -Levels: - summary - High-level overview (default) - detailed - Per-agent breakdown with key metrics - deep - Full message-level analysis - timeline - Chronological event trace - -Focus: - all - Full session analysis (default) - principal - Focus on Principal agent - partner - Focus on Partner agent - WM_N - Focus on specific work module (e.g., WM_1, WM_2) - errors - Focus on errors and issues - tokens - Focus on token utilization +Modes (what to analyze): + summary - High-level overview with issue detection (default) + detailed - Per-agent breakdown with key metrics and tool usage + tokens - Token utilization analysis with visual charts + handoff - Deliverable flow and handoff issue analysis + thrashing - Root cause analysis for duplicate dispatches + timeline - Chronological event trace + errors - Focus on errors and issues only + all - Run all analysis modes sequentially + ⚠️ WARNING: 'all' produces very large output that may crash + some environments (e.g., VS Code agent terminal). Call modes + individually instead, or redirect output to a file. + +Agent Filter (optional, narrows scope): + --agent principal - Focus on Principal agent + --agent partner - Focus on Partner agent + --agent WM_1 - Focus on specific work module (e.g., WM_1, WM_2) + +Output Options: + --json - Output as JSON instead of formatted text + --no-color - Disable colored output Examples: - python analyze_session.py http://localhost:3800/webview/r?id=tentacled-pearl-oriole python analyze_session.py tentacled-pearl-oriole - python analyze_session.py projects/MyProject/session-id.json - python analyze_session.py projects/MyProject/session-id.json --level detailed - python analyze_session.py projects/MyProject/session-id.json --level deep --focus WM_1 - python analyze_session.py projects/MyProject/session-id.json --focus tokens + python analyze_session.py tentacled-pearl-oriole --mode detailed + python analyze_session.py tentacled-pearl-oriole --mode tokens + python analyze_session.py tentacled-pearl-oriole --mode handoff + python analyze_session.py tentacled-pearl-oriole --mode thrashing + python analyze_session.py tentacled-pearl-oriole --mode all + python analyze_session.py tentacled-pearl-oriole --mode detailed --agent WM_1 + python analyze_session.py tentacled-pearl-oriole --json """ import argparse @@ -192,6 +203,7 @@ class WorkModuleSummary: tool_calls: Counter = field(default_factory=Counter) deliverables_count: int = 0 message_count: int = 0 + dispatch_count: int = 0 # How many times this module was dispatched (>1 = thrashing) dispatch_status: str = "unknown" created_at: Optional[str] = None updated_at: Optional[str] = None @@ -290,10 +302,28 @@ def analyze_messages(messages: List[Dict], model_name: str = "unknown") -> Tuple name = tc.get("function", {}).get("name", "unknown") tool_calls[name] += 1 - # Detect errors + # Detect errors - look for actual error indicators, not just the word "error" content_str = str(content).lower() - if "error" in content_str or "failed" in content_str: - if msg.get("role") == "tool": + if msg.get("role") == "tool": + # Check for actual error patterns, excluding success messages + is_error = False + if "overall status**: `success`" in content_str: + is_error = False # Not an error - it's a success report + elif any(pattern in content_str for pattern in [ + "tool_execution_failed", + "exception", + "traceback", + "status\": \"error", + "\"error\":", + "failed to", + "could not", + "unable to", + "error occurred", + "error:", + ]): + is_error = True + + if is_error: errors.append({ "type": "tool_error", "preview": str(content)[:200] @@ -383,16 +413,37 @@ def analyze_work_modules(team_state: Dict) -> Dict[str, WorkModuleSummary]: if assignee_history and isinstance(assignee_history[0], dict): summary.assigned_agent = assignee_history[0].get("agent", "unknown") - # Analyze context archive + # Analyze ALL context archives (important for modules dispatched multiple times) context_archive = wm.get("context_archive", []) - if isinstance(context_archive, list) and context_archive: - archive = context_archive[0] if isinstance(context_archive[0], dict) else {} + total_tokens = TokenMetrics() + total_tool_calls = Counter() + total_messages = 0 + total_deliverables = 0 + + for archive in context_archive: + if not isinstance(archive, dict): + continue messages = archive.get("messages", []) model = archive.get("model", DEFAULT_MODEL) - - summary.tokens, summary.tool_calls, _ = analyze_messages(messages, model) - summary.message_count = len(messages) - summary.deliverables_count = len(archive.get("deliverables", [])) + + archive_tokens, archive_tools, _ = analyze_messages(messages, model) + total_tokens.estimated_tokens += archive_tokens.estimated_tokens + total_tokens.message_count += archive_tokens.message_count + total_tool_calls.update(archive_tools) + total_messages += len(messages) + + # Count deliverables - check both dict format and list format + deliverables = archive.get("deliverables", {}) + if isinstance(deliverables, dict) and deliverables.get("primary_summary"): + total_deliverables += 1 + elif isinstance(deliverables, list): + total_deliverables += len(deliverables) + + summary.tokens = total_tokens + summary.tool_calls = total_tool_calls + summary.message_count = total_messages + summary.deliverables_count = total_deliverables + summary.dispatch_count = len(context_archive) # Track how many times dispatched summaries[wm_id] = summary @@ -630,7 +681,8 @@ def print_detailed(analysis: SessionAnalysis): if analysis.work_modules: print_subheader("WORK MODULE DETAILS") for wm_id, wm in sorted(analysis.work_modules.items()): - print(f"\n {Colors.BOLD}{wm_id}: {wm.name[:50]}{Colors.RESET}") + thrash_indicator = f" {Colors.RED}(dispatched {wm.dispatch_count}x!){Colors.RESET}" if wm.dispatch_count > 1 else "" + print(f"\n {Colors.BOLD}{wm_id}: {wm.name[:50]}{Colors.RESET}{thrash_indicator}") print(f" Profile: {Colors.CYAN}{wm.agent_profile}{Colors.RESET}") print(f" Status: {status_color(wm.status)}{wm.status}{Colors.RESET}") print(f" Dispatch: {status_color(wm.dispatch_status)}{wm.dispatch_status}{Colors.RESET}") @@ -642,68 +694,6 @@ def print_detailed(analysis: SessionAnalysis): print(f" Tools: {tools}") -def print_deep(analysis: SessionAnalysis, focus: str = "all", session_path: Path = None): - """Print deep level output with message-level details.""" - print_detailed(analysis) - - if not session_path: - print(f"\n{Colors.YELLOW}Note: Deep analysis requires session path for message details{Colors.RESET}") - return - - with open(session_path, 'r') as f: - data = json.load(f) - - sub_contexts = data.get("sub_contexts_state", {}) - - # Deep dive based on focus - if focus in ["all", "principal"] and analysis.principal: - print_subheader("PRINCIPAL MESSAGE TRACE") - principal_ctx = sub_contexts.get("_principal_context_ref", {}) - messages = principal_ctx.get("messages", []) - - # Show first 10 and last 10 messages - print(f"\n First 10 messages:") - for i, msg in enumerate(messages[:10]): - role = msg.get("role", "?") - tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] - tc_str = f" -> {tc}" if tc else "" - content_preview = str(msg.get("content", ""))[:80].replace("\n", " ") - print(f" [{i:4}] {role:10}{tc_str}") - - if len(messages) > 20: - print(f"\n ... {len(messages) - 20} messages omitted ...") - - print(f"\n Last 10 messages:") - for i, msg in enumerate(messages[-10:]): - idx = len(messages) - 10 + i - role = msg.get("role", "?") - tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] - tc_str = f" -> {tc}" if tc else "" - print(f" [{idx:4}] {role:10}{tc_str}") - - # Work module deep dive - if focus.startswith("WM_"): - team_state = data.get("team_state", {}) - work_modules = team_state.get("work_modules", {}) - wm = work_modules.get(focus) - if wm: - print_subheader(f"DEEP DIVE: {focus}") - context_archive = wm.get("context_archive", []) - if context_archive and isinstance(context_archive[0], dict): - archive = context_archive[0] - messages = archive.get("messages", []) - - print(f"\n All {len(messages)} messages:") - for i, msg in enumerate(messages): - role = msg.get("role", "?") - tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] - tc_str = f" -> {tc}" if tc else "" - content = str(msg.get("content", ""))[:100].replace("\n", " ") - print(f" [{i:3}] {role:10}{tc_str}") - if content and role == "assistant" and not tc: - print(f" {Colors.GRAY}{content}...{Colors.RESET}") - - def print_token_focus(analysis: SessionAnalysis): """Print token-focused analysis.""" print_header("TOKEN UTILIZATION ANALYSIS") @@ -749,6 +739,219 @@ def print_token_focus(analysis: SessionAnalysis): print(f" Overall utilization: {overall_util:.1f}%") +def print_handoff_analysis(analysis: SessionAnalysis, session_path: Path = None): + """ + Analyze delegation handoffs, message inheritance, and deliverable flow. + + This mode answers: + 1. Why did an agent not return deliverables properly? + 2. Why couldn't the principal access a subagent's messages? + 3. Why couldn't newly spawned subagents access earlier agent's messages? + """ + print_header("HANDOFF & DELIVERABLE FLOW ANALYSIS") + + if not session_path: + print(f"\n{Colors.YELLOW}Note: Handoff analysis requires session path{Colors.RESET}") + return + + with open(session_path, 'r') as f: + data = json.load(f) + + team_state = data.get("team_state", {}) + work_modules = team_state.get("work_modules", {}) + dispatch_history = team_state.get("dispatch_history", []) + + # ========================================================================== + # SECTION 1: Dispatch History Analysis + # ========================================================================== + print_subheader("1. DISPATCH HISTORY (Delegation Chain)") + + # Track duplicate dispatches + dispatch_counts = Counter(d.get("module_id") for d in dispatch_history) + duplicates = {k: v for k, v in dispatch_counts.items() if v > 1} + + if duplicates: + print(f"\n {Colors.RED}⚠ DUPLICATE DISPATCHES DETECTED:{Colors.RESET}") + for module_id, count in duplicates.items(): + print(f" {module_id} dispatched {count} times (possible thrashing)") + + print(f"\n {Colors.BOLD}Dispatch Sequence:{Colors.RESET}") + for i, dispatch in enumerate(dispatch_history): + module_id = dispatch.get("module_id", "?") + status = dispatch.get("status", "unknown") + profile = dispatch.get("profile_logical_name", "unknown") + timestamp = dispatch.get("timestamp", dispatch.get("dispatched_at", ""))[:19] + color = status_color(status) + + # Check for notes_from_principal + notes = dispatch.get("notes_from_principal", "") + notes_preview = f" | notes: {notes[:60]}..." if notes else "" + + print(f"\n {Colors.GRAY}[{i+1}] {timestamp}{Colors.RESET}") + print(f" Module: {module_id} -> Profile: {Colors.CYAN}{profile}{Colors.RESET}") + print(f" Status: {color}{status}{Colors.RESET}{notes_preview}") + + # ========================================================================== + # SECTION 2: Work Module Deliverables Analysis + # ========================================================================== + print_subheader("2. DELIVERABLE EXTRACTION ANALYSIS") + + for wm_id, wm in sorted(work_modules.items()): + wm_name = wm.get("name", "unnamed")[:50] + status = wm.get("status", "unknown") + + # Check deliverables array (what Principal sees) + deliverables_arr = wm.get("deliverables", []) + + # Check context_archive (what was actually produced) + context_archive = wm.get("context_archive", []) + archived_deliverables = [] + archived_messages = [] + + for archive in context_archive: + if isinstance(archive, dict): + arch_del = archive.get("deliverables", {}) + if arch_del: + archived_deliverables.append(arch_del) + arch_msgs = archive.get("messages", []) + archived_messages.extend(arch_msgs) + + print(f"\n {Colors.BOLD}{wm_id}: {wm_name}{Colors.RESET}") + print(f" Status: {status_color(status)}{status}{Colors.RESET}") + + # Deliverables array check + if deliverables_arr: + print(f" {Colors.GREEN}βœ“ deliverables[] has {len(deliverables_arr)} items{Colors.RESET}") + else: + print(f" {Colors.YELLOW}⚠ deliverables[] is EMPTY{Colors.RESET}") + + # Context archive check + if archived_deliverables: + for j, ad in enumerate(archived_deliverables): + primary = ad.get("primary_summary", "") + print(f" {Colors.GREEN}βœ“ context_archive[{j}].deliverables.primary_summary: {len(primary)} chars{Colors.RESET}") + if primary: + preview = primary[:150].replace("\n", " ") + print(f" Preview: {Colors.GRAY}{preview}...{Colors.RESET}") + else: + print(f" {Colors.RED}βœ— No deliverables in context_archive{Colors.RESET}") + + # Check for finish_flow in messages (did agent properly finish?) + finish_calls = [m for m in archived_messages if any( + tc.get("function", {}).get("name") == "finish_flow" + for tc in m.get("tool_calls", []) + )] + if finish_calls: + print(f" {Colors.GREEN}βœ“ finish_flow called {len(finish_calls)} time(s){Colors.RESET}") + else: + print(f" {Colors.RED}βœ— finish_flow NOT called - agent may not have completed properly{Colors.RESET}") + + # ========================================================================== + # SECTION 3: Message Inheritance Analysis + # ========================================================================== + print_subheader("3. MESSAGE INHERITANCE CHAIN") + + # Check what messages each work module inherited + for wm_id, wm in sorted(work_modules.items()): + context_archive = wm.get("context_archive", []) + if not context_archive: + continue + + for arch_idx, archive in enumerate(context_archive): + if not isinstance(archive, dict): + continue + + messages = archive.get("messages", []) + if not messages: + continue + + # First message is typically the briefing/inherited content + first_msg = messages[0] if messages else {} + first_content = str(first_msg.get("content", "")) + + print(f"\n {Colors.BOLD}{wm_id} (archive {arch_idx}):{Colors.RESET}") + print(f" Total messages: {len(messages)}") + + # Check for inherited message markers + if "inherit" in first_content.lower() or "previous" in first_content.lower(): + print(f" {Colors.GREEN}βœ“ Appears to have inherited context{Colors.RESET}") + + # Look for references to other work modules + other_wm_refs = re.findall(r'WM_\d+', first_content) + if other_wm_refs: + print(f" References to other modules: {', '.join(set(other_wm_refs))}") + + # Check first message length (briefing size) + briefing_size = len(first_content) + print(f" Initial briefing size: {briefing_size:,} chars (~{briefing_size//4:,} tokens)") + + # Check if briefing mentions deliverables from previous agents + if "deliverable" in first_content.lower(): + print(f" {Colors.GREEN}βœ“ Briefing mentions deliverables{Colors.RESET}") + else: + print(f" {Colors.YELLOW}⚠ Briefing does NOT mention deliverables{Colors.RESET}") + + # ========================================================================== + # SECTION 4: Potential Issues Summary + # ========================================================================== + print_subheader("4. HANDOFF ISSUES DETECTED") + + issues_found = [] + + # Check for duplicate dispatches + if duplicates: + issues_found.append({ + "severity": "HIGH", + "type": "duplicate_dispatch", + "details": f"Modules dispatched multiple times: {list(duplicates.keys())} - indicates thrashing" + }) + + # Check for empty deliverables on completed modules + for wm_id, wm in work_modules.items(): + if wm.get("status") in ["completed", "pending_review"]: + if not wm.get("deliverables"): + # Check if archive has deliverables (data model mismatch) + context_archive = wm.get("context_archive", []) + has_archived = any( + isinstance(a, dict) and a.get("deliverables", {}).get("primary_summary") + for a in context_archive + ) + if has_archived: + issues_found.append({ + "severity": "MEDIUM", + "type": "deliverable_not_propagated", + "details": f"{wm_id}: Deliverables exist in context_archive but NOT in work_modules.deliverables[] - Principal may not see them" + }) + else: + issues_found.append({ + "severity": "HIGH", + "type": "no_deliverables", + "details": f"{wm_id}: Completed but NO deliverables anywhere" + }) + + # Check dispatch vs completion status mismatch + for dispatch in dispatch_history: + module_id = dispatch.get("module_id") + dispatch_status = dispatch.get("status", "") + if module_id in work_modules: + wm_status = work_modules[module_id].get("status", "") + if "RUNNING" in dispatch_status and wm_status == "completed": + issues_found.append({ + "severity": "LOW", + "type": "status_mismatch", + "details": f"{module_id}: dispatch_history says RUNNING but work_module says completed" + }) + + if issues_found: + for issue in issues_found: + sev = issue["severity"] + sev_color = Colors.RED if sev == "HIGH" else (Colors.YELLOW if sev == "MEDIUM" else Colors.GRAY) + print(f"\n {sev_color}[{sev}]{Colors.RESET} {issue['type']}") + print(f" {issue['details']}") + else: + print(f"\n {Colors.GREEN}βœ“ No handoff issues detected{Colors.RESET}") + + def print_timeline(analysis: SessionAnalysis, session_path: Path = None): """Print chronological event timeline.""" print_header("SESSION TIMELINE") @@ -818,6 +1021,371 @@ def print_timeline(analysis: SessionAnalysis, session_path: Path = None): print(f" {i+1}. {dispatch.get('module_id', '?'):10} {color}{status}{Colors.RESET}") +def print_thrashing_analysis(analysis: SessionAnalysis, session_path: Path = None): + """ + Analyze WHY thrashing occurred - trace Principal's decision-making. + + Shows: + 1. Principal's tool calls leading up to each duplicate dispatch + 2. What information Principal had when making decisions + 3. Why Principal thought work wasn't done + """ + print_header("THRASHING ROOT CAUSE ANALYSIS") + + if not session_path: + print(f"\n{Colors.YELLOW}Note: Thrashing analysis requires session path{Colors.RESET}") + return + + with open(session_path, 'r') as f: + data = json.load(f) + + team_state = data.get("team_state", {}) + sub_contexts = data.get("sub_contexts_state", {}) + dispatch_history = team_state.get("dispatch_history", []) + work_modules = team_state.get("work_modules", {}) + + # Find duplicate dispatches + dispatch_counts = Counter(d.get("module_id") for d in dispatch_history) + duplicates = {mid: count for mid, count in dispatch_counts.items() if count > 1} + + if not duplicates: + print(f"\n{Colors.GREEN}βœ“ No duplicate dispatches found (no thrashing){Colors.RESET}") + return + + print_subheader("1. DUPLICATE DISPATCH SUMMARY") + for mid, count in duplicates.items(): + print(f"\n {Colors.RED}⚠ {mid}{Colors.RESET} dispatched {count} times") + # Show each dispatch + for i, dispatch in enumerate(dispatch_history): + if dispatch.get("module_id") == mid: + ts = dispatch.get("start_timestamp", "?")[:19] + status = dispatch.get("status", "?") + profile = dispatch.get("profile_logical_name", "?") + color = status_color(status) + print(f" [{i+1}] {ts} -> {profile} -> {color}{status}{Colors.RESET}") + + # Analyze Principal's messages around dispatch decisions + print_subheader("2. PRINCIPAL DECISION TRACE") + principal_ctx = sub_contexts.get("_principal_context_ref", {}) + principal_messages = principal_ctx.get("messages", []) + + # Find dispatch_work_modules tool calls + dispatch_calls = [] + for i, msg in enumerate(principal_messages): + if msg.get("role") == "assistant": + for tc in msg.get("tool_calls", []): + func_name = tc.get("function", {}).get("name", "") + if func_name == "dispatch_work_modules": + try: + args = json.loads(tc.get("function", {}).get("arguments", "{}")) + dispatch_calls.append({ + "msg_index": i, + "args": args, + "tool_call_id": tc.get("id") + }) + except: + pass + + print(f"\n Found {len(dispatch_calls)} dispatch_work_modules calls:") + + for dc in dispatch_calls: + idx = dc["msg_index"] + args = dc["args"] + dispatches = args.get("dispatches", []) + + print(f"\n {Colors.CYAN}Message #{idx}{Colors.RESET}") + + # Show what modules were being dispatched + for d in dispatches: + mid = d.get("module_id_to_assign", "?") + inherit = d.get("inherit_messages_from", []) + is_duplicate = mid in duplicates + dup_marker = f" {Colors.RED}(DUPLICATE){Colors.RESET}" if is_duplicate else "" + print(f" -> Dispatching: {mid}{dup_marker}") + if inherit: + print(f" Inheriting from: {inherit}") + + # Look at the assistant message content before the dispatch + if idx > 0: + prev_msg = principal_messages[idx] + content = prev_msg.get("content", "") + if content: + # Find relevant snippets about the module + for mid in duplicates: + if mid in str(content): + # Extract context around the mention + lines = str(content).split('\n') + relevant = [l for l in lines if mid in l][:5] + if relevant: + print(f" {Colors.GRAY}Principal's reasoning about {mid}:{Colors.RESET}") + for line in relevant: + print(f" {line[:100]}...") + + # Check what the tool results looked like + print_subheader("3. TOOL RESULTS PRINCIPAL SAW") + + for mid in duplicates: + print(f"\n {Colors.BOLD}{mid}{Colors.RESET}:") + + # Find tool results for this module + relevant_results = [] + for i, msg in enumerate(principal_messages): + if msg.get("role") == "tool": + content = str(msg.get("content", "")) + if mid in content: + tool_id = msg.get("tool_call_id", "?") + preview = content[:300].replace('\n', ' ') + relevant_results.append({ + "index": i, + "tool_id": tool_id, + "preview": preview + }) + + if relevant_results: + for r in relevant_results[:3]: # Show first 3 + print(f" [msg {r['index']}] {r['preview'][:200]}...") + else: + print(f" {Colors.YELLOW}No tool results found mentioning {mid}{Colors.RESET}") + + # Check work module status at end + print_subheader("4. FINAL WORK MODULE STATE") + for mid in duplicates: + wm = work_modules.get(mid, {}) + status = wm.get("status", "?") + archives = len(wm.get("context_archive", [])) + deliverables = wm.get("deliverables", []) + + print(f"\n {mid}:") + print(f" Status: {status_color(status)}{status}{Colors.RESET}") + print(f" Context archives: {archives}") + print(f" work_modules.deliverables[]: {len(deliverables)} items") + + # Check what's in context_archive + for i, arch in enumerate(wm.get("context_archive", [])): + del_dict = arch.get("deliverables", {}) + summary = del_dict.get("primary_summary", "") + print(f" Archive[{i}]: deliverables.primary_summary = {len(summary)} chars") + + # Diagnosis + print_subheader("5. ROOT CAUSE DIAGNOSIS") + + # Check if deliverables were in wrong location + for mid in duplicates: + wm = work_modules.get(mid, {}) + has_archive_deliverables = any( + arch.get("deliverables", {}).get("primary_summary") + for arch in wm.get("context_archive", []) + ) + has_top_level_deliverables = len(wm.get("deliverables", [])) > 0 + + if has_archive_deliverables and not has_top_level_deliverables: + print(f"\n {Colors.RED}[DATA MODEL ISSUE]{Colors.RESET} {mid}:") + print(f" Deliverables ARE in context_archive (correct for inheritance)") + print(f" But work_modules[{mid}].deliverables[] is empty (legacy field)") + print(f" {Colors.YELLOW}This is expected - the system reads from context_archive{Colors.RESET}") + + # Check for flow_decider issues + flow_decider_calls = sum(1 for msg in principal_messages + if msg.get("role") == "tool" and + "flow_decider" in str(msg.get("name", ""))) + + if flow_decider_calls > 0: + print(f"\n Flow decider invocations: {flow_decider_calls}") + + # Check for empty LLM responses + empty_responses = sum(1 for msg in principal_messages + if msg.get("role") == "assistant" and + not msg.get("content") and + not msg.get("tool_calls")) + + if empty_responses > 0: + print(f"\n {Colors.YELLOW}[LLM ISSUE]{Colors.RESET} Empty assistant responses: {empty_responses}") + print(f" May indicate model confusion or prompt issues") + + +def print_errors(analysis: SessionAnalysis, session_path: Path = None): + """Print error-focused analysis.""" + print_header("ERROR ANALYSIS") + + # Show detected issues from analysis + if analysis.issues: + print_subheader(f"DETECTED ISSUES ({len(analysis.issues)})") + for issue in analysis.issues: + sev = issue["severity"] + sev_color = Colors.RED if sev == "HIGH" else (Colors.YELLOW if sev == "MEDIUM" else Colors.GRAY) + print(f"\n {sev_color}[{sev}]{Colors.RESET} {issue['type']}") + print(f" Agent: {issue['agent']}") + print(f" {issue['details']}") + else: + print(f"\n{Colors.GREEN}βœ“ No issues detected in analysis{Colors.RESET}") + + # Show agent errors + if analysis.principal and analysis.principal.errors: + print_subheader(f"PRINCIPAL ERRORS ({len(analysis.principal.errors)})") + for err in analysis.principal.errors[:10]: + print(f"\n {Colors.RED}[{err['type']}]{Colors.RESET}") + print(f" {err['preview'][:200]}...") + + if analysis.partner and analysis.partner.errors: + print_subheader(f"PARTNER ERRORS ({len(analysis.partner.errors)})") + for err in analysis.partner.errors[:10]: + print(f"\n {Colors.RED}[{err['type']}]{Colors.RESET}") + print(f" {err['preview'][:200]}...") + + # Scan for errors in work modules + if session_path: + with open(session_path, 'r') as f: + data = json.load(f) + + team_state = data.get("team_state", {}) + work_modules = team_state.get("work_modules", {}) + + for wm_id, wm in work_modules.items(): + context_archive = wm.get("context_archive", []) + wm_errors = [] + + for archive in context_archive: + if not isinstance(archive, dict): + continue + messages = archive.get("messages", []) + for msg in messages: + if msg.get("role") == "tool": + content = str(msg.get("content", "")).lower() + if "error" in content or "failed" in content or "exception" in content: + wm_errors.append(str(msg.get("content", ""))[:200]) + + if wm_errors: + print_subheader(f"{wm_id} ERRORS ({len(wm_errors)})") + for err in wm_errors[:5]: + print(f"\n {Colors.RED}β€’{Colors.RESET} {err}...") + + # Summary + total_errors = len(analysis.issues) + if analysis.principal: + total_errors += len(analysis.principal.errors) + if analysis.partner: + total_errors += len(analysis.partner.errors) + + print_subheader("SUMMARY") + if total_errors == 0: + print(f"\n {Colors.GREEN}βœ“ No errors found in session{Colors.RESET}") + else: + print(f"\n {Colors.RED}Total errors/issues: {total_errors}{Colors.RESET}") + + +def print_agent_detail(analysis: SessionAnalysis, agent_filter: str, session_path: Path = None): + """Print detailed analysis for a specific agent.""" + + if agent_filter == "principal": + if not analysis.principal: + print(f"{Colors.YELLOW}No Principal agent found in session{Colors.RESET}") + return + + print_header("PRINCIPAL AGENT ANALYSIS") + p = analysis.principal + print(f"\n{Colors.BOLD}Agent Info:{Colors.RESET}") + print(f" Model: {p.model}") + print(f" Messages: {p.tokens.message_count}") + print(f" Tokens: {format_tokens(p.tokens)}") + + print(f"\n{Colors.BOLD}Tool Usage:{Colors.RESET}") + for tool, count in p.tool_calls.most_common(): + bar = "β–ˆ" * min(count // 2, 40) + print(f" {tool:40} {count:5} {Colors.GRAY}{bar}{Colors.RESET}") + + if p.errors: + print(f"\n{Colors.RED}Errors ({len(p.errors)}):{Colors.RESET}") + for err in p.errors[:5]: + print(f" - {err['type']}: {err['preview'][:100]}...") + + # Show message trace if session available + if session_path: + with open(session_path, 'r') as f: + data = json.load(f) + sub_contexts = data.get("sub_contexts_state", {}) + principal_ctx = sub_contexts.get("_principal_context_ref", {}) + messages = principal_ctx.get("messages", []) + + print_subheader(f"MESSAGE TRACE ({len(messages)} messages)") + for i, msg in enumerate(messages[:15]): + role = msg.get("role", "?") + tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] + tc_str = f" -> {tc}" if tc else "" + print(f" [{i:3}] {role:10}{tc_str}") + if len(messages) > 15: + print(f" ... {len(messages) - 15} more messages") + + elif agent_filter == "partner": + if not analysis.partner: + print(f"{Colors.YELLOW}No Partner agent found in session{Colors.RESET}") + return + + print_header("PARTNER AGENT ANALYSIS") + p = analysis.partner + print(f"\n{Colors.BOLD}Agent Info:{Colors.RESET}") + print(f" Model: {p.model}") + print(f" Messages: {p.tokens.message_count}") + print(f" Tokens: {format_tokens(p.tokens)}") + + if p.tool_calls: + print(f"\n{Colors.BOLD}Tool Usage:{Colors.RESET}") + for tool, count in p.tool_calls.most_common(): + print(f" {tool}: {count}") + + elif agent_filter.startswith("WM_"): + wm = analysis.work_modules.get(agent_filter) + if not wm: + print(f"{Colors.YELLOW}Work module {agent_filter} not found{Colors.RESET}") + return + + print_header(f"WORK MODULE: {agent_filter}") + print(f"\n{Colors.BOLD}Module Info:{Colors.RESET}") + print(f" Name: {wm.name}") + print(f" Description: {wm.description}") + print(f" Profile: {Colors.CYAN}{wm.agent_profile}{Colors.RESET}") + print(f" Status: {status_color(wm.status)}{wm.status}{Colors.RESET}") + print(f" Dispatch: {status_color(wm.dispatch_status)}{wm.dispatch_status}{Colors.RESET}") + print(f" Messages: {wm.message_count}") + print(f" Tokens: {format_tokens(wm.tokens)}") + print(f" Deliverables: {wm.deliverables_count}") + + if wm.tool_calls: + print(f"\n{Colors.BOLD}Tool Usage:{Colors.RESET}") + for tool, count in wm.tool_calls.most_common(): + print(f" {tool}: {count}") + + # Show message trace from context_archive + if session_path: + with open(session_path, 'r') as f: + data = json.load(f) + team_state = data.get("team_state", {}) + work_modules = team_state.get("work_modules", {}) + wm_data = work_modules.get(agent_filter, {}) + context_archive = wm_data.get("context_archive", []) + + for arch_idx, archive in enumerate(context_archive): + if not isinstance(archive, dict): + continue + messages = archive.get("messages", []) + deliverables = archive.get("deliverables", {}) + + print_subheader(f"ARCHIVE {arch_idx} ({len(messages)} messages)") + + for i, msg in enumerate(messages): + role = msg.get("role", "?") + tc = [tc.get("function", {}).get("name") for tc in msg.get("tool_calls", [])] + tc_str = f" -> {tc}" if tc else "" + print(f" [{i:3}] {role:10}{tc_str}") + + if deliverables.get("primary_summary"): + summary = deliverables["primary_summary"] + print(f"\n {Colors.GREEN}Deliverable ({len(summary)} chars):{Colors.RESET}") + print(f" {Colors.GRAY}{summary[:300]}...{Colors.RESET}") + else: + print(f"{Colors.RED}Unknown agent filter: {agent_filter}{Colors.RESET}") + print(f"Use: principal, partner, or WM_N (e.g., WM_1, WM_2)") + + # ============================================================================= # MAIN # ============================================================================= @@ -829,20 +1397,35 @@ def main(): epilog=__doc__ ) parser.add_argument("session_input", - help="Session file path, URL (http://localhost:3800/webview/r?id=SESSION_ID), or session ID") - parser.add_argument("--level", "-l", - choices=["summary", "detailed", "deep", "timeline"], + help="Session file path, URL, or session ID") + parser.add_argument("--mode", "-m", + choices=["summary", "detailed", "tokens", "handoff", "thrashing", "timeline", "errors", "all"], default="summary", - help="Analysis detail level (default: summary)") - parser.add_argument("--focus", "-f", - default="all", - help="Focus area: all, principal, partner, WM_N, errors, tokens") + help="Analysis mode (default: summary)") + parser.add_argument("--agent", "-a", + default=None, + help="Filter to specific agent: principal, partner, or WM_N") parser.add_argument("--no-color", action="store_true", help="Disable colored output") parser.add_argument("--json", action="store_true", help="Output as JSON instead of formatted text") + + # Legacy support for old arguments + parser.add_argument("--level", "-l", dest="legacy_level", default=None, help=argparse.SUPPRESS) + parser.add_argument("--focus", "-f", dest="legacy_focus", default=None, help=argparse.SUPPRESS) args = parser.parse_args() + + # Handle legacy arguments + if args.legacy_level or args.legacy_focus: + print(f"{Colors.YELLOW}Note: --level and --focus are deprecated. Use --mode and --agent instead.{Colors.RESET}\n") + if args.legacy_level in ["detailed", "deep", "timeline"]: + args.mode = args.legacy_level if args.legacy_level != "deep" else "detailed" + if args.legacy_focus and args.legacy_focus != "all": + if args.legacy_focus in ["tokens", "handoff", "thrashing", "errors"]: + args.mode = args.legacy_focus + elif args.legacy_focus in ["principal", "partner"] or args.legacy_focus.startswith("WM_"): + args.agent = args.legacy_focus # Disable colors if requested if args.no_color: @@ -868,9 +1451,8 @@ def main(): print(f"{Colors.RED}Error analyzing session: {e}{Colors.RESET}") raise - # Output based on format + # JSON output if args.json: - # Convert to JSON-serializable dict output = { "session_id": analysis.session_id, "run_type": analysis.run_type, @@ -898,6 +1480,8 @@ def main(): "dispatch_status": wm.dispatch_status, "tokens": wm.tokens.estimated_tokens, "messages": wm.message_count, + "dispatch_count": wm.dispatch_count, + "deliverables_count": wm.deliverables_count, } for wm_id, wm in analysis.work_modules.items() } @@ -905,15 +1489,33 @@ def main(): print(json.dumps(output, indent=2)) return - # Formatted output based on level and focus - if args.focus == "tokens": + # If agent filter specified, show agent-specific detail + if args.agent: + print_agent_detail(analysis, args.agent, session_path) + return + + # Mode-based output + if args.mode == "all": + print_detailed(analysis) # includes summary print_token_focus(analysis) - elif args.level == "timeline": + print_handoff_analysis(analysis, session_path) + print_thrashing_analysis(analysis, session_path) + print_errors(analysis, session_path) print_timeline(analysis, session_path) - elif args.level == "deep": - print_deep(analysis, args.focus, session_path) - elif args.level == "detailed": + elif args.mode == "summary": + print_summary(analysis) + elif args.mode == "detailed": print_detailed(analysis) + elif args.mode == "tokens": + print_token_focus(analysis) + elif args.mode == "handoff": + print_handoff_analysis(analysis, session_path) + elif args.mode == "thrashing": + print_thrashing_analysis(analysis, session_path) + elif args.mode == "timeline": + print_timeline(analysis, session_path) + elif args.mode == "errors": + print_errors(analysis, session_path) else: print_summary(analysis) From 761b130e00dbd69f30c1a09411818376b147a3c7 Mon Sep 17 00:00:00 2001 From: Myles Dear Date: Wed, 31 Dec 2025 00:16:47 -0500 Subject: [PATCH 3/9] feat(frontend): improve FlowView zoom, scroll, and layout behavior Flow visualization improvements: - Add dynamic minZoom that adapts to card count (see all cards at min zoom) - Fix maxZoom at 1.5x for readable card text regardless of card count - Align scroll wheel zoom speed between minimap and canvas (~9 clicks) - Add translateExtent to constrain panning within node bounds - Add status-based MiniMap colors (blue=running, green=success, red=error) Scroll and layout fixes: - Fix page-level scrolling by adding overflow:hidden to html/body/SidebarProvider - Fix auto-scroll on page load (scrollIntoView block:'nearest') - Add overscroll-contain to ChatHistory to prevent scroll chaining Swim lane layout (flow-utils.ts): - Rewrite layout algorithm for fixed-width swim lanes per agent - Increase node fallback dimensions for better readability - Add minimum dimension enforcement in getNodeSize() Files changed: - FlowView.tsx: zoom config, MiniMap styling, ReactFlowProvider wrapper - ChatLayout.tsx: overflow-hidden on panels - Workspace.tsx: overflow-hidden on container - ChatHistory.tsx: overscroll-contain - flow-utils.ts: swim lane algorithm - globals.css: html/body overflow hidden - layout.tsx: SidebarProvider height constraints - r/page.tsx: scrollIntoView fix --- frontend/app/chat/components/ChatHistory.tsx | 2 +- frontend/app/chat/components/ChatLayout.tsx | 8 +- frontend/app/chat/components/FlowView.tsx | 112 +++++++++++++-- frontend/app/chat/components/Workspace.tsx | 2 +- frontend/app/chat/lib/flow-utils.ts | 140 +++++++++---------- frontend/app/globals.css | 8 ++ frontend/app/layout.tsx | 6 +- frontend/app/r/page.tsx | 3 +- 8 files changed, 185 insertions(+), 96 deletions(-) diff --git a/frontend/app/chat/components/ChatHistory.tsx b/frontend/app/chat/components/ChatHistory.tsx index 5772da4..17d6802 100644 --- a/frontend/app/chat/components/ChatHistory.tsx +++ b/frontend/app/chat/components/ChatHistory.tsx @@ -10,7 +10,7 @@ interface ChatHistoryProps { export const ChatHistory = observer(({ messages, messagesEndRef }: ChatHistoryProps) => { return ( -
+
{messages.map((turn) => ( +
@@ -45,13 +45,13 @@ export const ChatLayout = observer(function ChatLayout(props: ChatLayoutProps) {
-
+
- + { +// Inner component that can use useReactFlow hook +const FlowViewInner = observer(({ onNodeClick }: FlowViewProps) => { const { nodes, edges, onNodeSizesChange } = useFlowView(sessionStore.flowStructure); const proOptions = { hideAttribution: true }; @@ -345,6 +348,58 @@ export const FlowView = observer(({ onNodeClick }: FlowViewProps) => { [] ); + // Calculate the bounding box of all nodes + const nodeBounds = useMemo(() => { + if (nodes.length === 0) { + return { minX: 0, minY: 0, maxX: 1000, maxY: 800, width: 1000, height: 800 }; + } + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + nodes.forEach(node => { + const x = node.position.x; + const y = node.position.y; + const width = node.width || 380; + const height = node.height || 200; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + width); + maxY = Math.max(maxY, y + height); + }); + + return { + minX, minY, maxX, maxY, + width: maxX - minX, + height: maxY - minY + }; + }, [nodes]); + + // Calculate translate extent to constrain panning within reasonable bounds around nodes + // This prevents users from panning into infinite empty space, which causes minimap zoom-out + const translateExtent = useMemo((): [[number, number], [number, number]] => { + const padding = 500; + return [ + [nodeBounds.minX - padding, nodeBounds.minY - padding], + [nodeBounds.maxX + padding, nodeBounds.maxY + padding] + ]; + }, [nodeBounds]); + + // Calculate dynamic minZoom based on content size + // This ensures "zoom out all the way" shows all cards + const dynamicMinZoom = useMemo(() => { + // Assume a typical viewport of ~800x600 for calculation + // The actual fitView will handle the real viewport + const viewportWidth = 1200; + const viewportHeight = 800; + const padding = 0.2; // 20% padding around content + + const scaleX = viewportWidth / (nodeBounds.width * (1 + padding)); + const scaleY = viewportHeight / (nodeBounds.height * (1 + padding)); + const fitZoom = Math.min(scaleX, scaleY); + + // Don't go below 0.05 (5%) or above 0.5 for minZoom + return Math.max(0.05, Math.min(0.5, fitZoom * 0.8)); + }, [nodeBounds]); + const viewError = sessionStore.viewErrors.get('flow_view'); if (viewError) { @@ -364,6 +419,9 @@ export const FlowView = observer(({ onNodeClick }: FlowViewProps) => { ); } + // maxZoom of 1.5 allows reading card text comfortably (150% of actual size) + const maxZoom = 1.5; + return ( { defaultEdgeOptions={{ type: 'custom' }} fitView={shouldFitView} fitViewOptions={{ - padding: 0.3, // increase padding to 30% - maxZoom: 1.0, // limit max zoom to 1:1 to prevent nodes from getting too large - minZoom: 0.1 // allow zooming out to 10% + padding: 0.3, // 30% padding around content + maxZoom: 1.0, // fitView won't zoom past 100% + minZoom: dynamicMinZoom }} - defaultViewport={{ x: 0, y: 0, zoom: 0.8 }} // Set default zoom to 80% - minZoom={0.1} - maxZoom={2.0} + defaultViewport={{ x: 0, y: 0, zoom: 0.8 }} + minZoom={dynamicMinZoom} // Dynamic: adapts to show all cards + maxZoom={maxZoom} // Fixed: readable card text at 150% + translateExtent={translateExtent} className="bg-white rounded-lg border border-[#E4E4E4]" nodesDraggable={false} nodesConnectable={false} elementsSelectable={true} panOnDrag={true} - zoomOnScroll={true} - preventScrolling={false} + zoomOnScroll={true} // Default canvas zoom (~9 clicks min to max) + zoomOnDoubleClick={false} + preventScrolling={true} proOptions={proOptions} onNodeClick={onNodeClick} nodesFocusable={true} @@ -395,7 +455,37 @@ export const FlowView = observer(({ onNodeClick }: FlowViewProps) => { > - (n.type === 'custom' ? '#fff' : '#eee')} zoomable={true} pannable={true} /> + { + // Color nodes based on their status for better visibility + const status = n.data?.status; + if (status === 'running') return '#3b82f6'; // blue + if (status === 'completed_success') return '#22c55e'; // green + if (status === 'completed_error') return '#ef4444'; // red + return '#94a3b8'; // slate for idle/default + }} + nodeStrokeColor="#64748b" + nodeStrokeWidth={2} + nodeBorderRadius={4} + maskColor="rgba(0, 0, 0, 0.1)" + zoomable={true} + zoomStep={1} // Match canvas: ~9 clicks min to max (default 10 is too fast) + pannable={true} + style={{ + backgroundColor: '#f8fafc', + border: '1px solid #e2e8f0', + borderRadius: '4px' + }} + /> ); }); + +// Main export - wraps with ReactFlowProvider so inner components can use useReactFlow +export const FlowView = observer(({ onNodeClick }: FlowViewProps) => { + return ( + + + + ); +}); diff --git a/frontend/app/chat/components/Workspace.tsx b/frontend/app/chat/components/Workspace.tsx index f001f53..b5e36f0 100644 --- a/frontend/app/chat/components/Workspace.tsx +++ b/frontend/app/chat/components/Workspace.tsx @@ -61,7 +61,7 @@ export const Workspace = observer((props: WorkspaceProps) => { // and its logic is now centralized in `sessionStore.ts` to prevent race conditions. return ( -
+