From 91f3eae553326a01cd91e61cabee8a2e367583a4 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Wed, 6 May 2026 21:37:02 +0200 Subject: [PATCH 01/16] Fix mistral wf skill --- .claude/skills/workflows/SKILL.md | 105 ++ .../workflows/references/execution_ids.md | 202 +++ .../getting-started/core-concepts.mdx | 192 +++ .../getting-started/installation.mdx | 93 + .../getting-started/introduction.mdx | 30 + .../references/getting-started/python-sdk.mdx | 257 +++ .../getting-started/value-proposition.mdx | 333 ++++ .../getting-started/your-first-workflow.mdx | 93 + .../guides/_deployment-patterns.mdx | 78 + .../references/guides/activities.mdx | 373 ++++ .../references/guides/assist-workflows.mdx | 1045 ++++++++++++ .../references/guides/concurrency.mdx | 487 ++++++ .../guides/dependency-injection.mdx | 185 ++ .../references/guides/durable-agents.mdx | 403 +++++ .../references/guides/error-codes.mdx | 219 +++ .../references/guides/handling-large-data.mdx | 140 ++ .../references/guides/limitations.mdx | 255 +++ .../references/guides/local-execution.mdx | 172 ++ .../references/guides/migration-v2-to-v3.mdx | 213 +++ .../references/guides/observability.mdx | 45 + .../references/guides/payload-encoding.mdx | 251 +++ .../references/guides/rate-limiting.mdx | 58 + .../references/guides/scheduling.mdx | 115 ++ .../guides/signals-queries-updates.mdx | 202 +++ .../guides/streaming-consumption.mdx | 401 +++++ .../workflows/references/guides/streaming.mdx | 148 ++ .../workflows/references/guides/testing.md | 144 ++ .../references/guides/workflows-exception.mdx | 176 ++ .../references/guides/workflows-plugins.mdx | 201 +++ .../workflows/references/guides/workflows.mdx | 626 +++++++ .../workflows/references/pipeline_pattern.md | 236 +++ .../workflows/references/workflow_testing.md | 173 ++ .../skills/workflows/scripts/test_workflow.py | 481 ++++++ pyproject.toml | 5 +- uv.lock | 1502 +++-------------- 35 files changed, 8392 insertions(+), 1247 deletions(-) create mode 100644 .claude/skills/workflows/SKILL.md create mode 100644 .claude/skills/workflows/references/execution_ids.md create mode 100644 .claude/skills/workflows/references/getting-started/core-concepts.mdx create mode 100644 .claude/skills/workflows/references/getting-started/installation.mdx create mode 100644 .claude/skills/workflows/references/getting-started/introduction.mdx create mode 100644 .claude/skills/workflows/references/getting-started/python-sdk.mdx create mode 100644 .claude/skills/workflows/references/getting-started/value-proposition.mdx create mode 100644 .claude/skills/workflows/references/getting-started/your-first-workflow.mdx create mode 100644 .claude/skills/workflows/references/guides/_deployment-patterns.mdx create mode 100644 .claude/skills/workflows/references/guides/activities.mdx create mode 100644 .claude/skills/workflows/references/guides/assist-workflows.mdx create mode 100644 .claude/skills/workflows/references/guides/concurrency.mdx create mode 100644 .claude/skills/workflows/references/guides/dependency-injection.mdx create mode 100644 .claude/skills/workflows/references/guides/durable-agents.mdx create mode 100644 .claude/skills/workflows/references/guides/error-codes.mdx create mode 100644 .claude/skills/workflows/references/guides/handling-large-data.mdx create mode 100644 .claude/skills/workflows/references/guides/limitations.mdx create mode 100644 .claude/skills/workflows/references/guides/local-execution.mdx create mode 100644 .claude/skills/workflows/references/guides/migration-v2-to-v3.mdx create mode 100644 .claude/skills/workflows/references/guides/observability.mdx create mode 100644 .claude/skills/workflows/references/guides/payload-encoding.mdx create mode 100644 .claude/skills/workflows/references/guides/rate-limiting.mdx create mode 100644 .claude/skills/workflows/references/guides/scheduling.mdx create mode 100644 .claude/skills/workflows/references/guides/signals-queries-updates.mdx create mode 100644 .claude/skills/workflows/references/guides/streaming-consumption.mdx create mode 100644 .claude/skills/workflows/references/guides/streaming.mdx create mode 100644 .claude/skills/workflows/references/guides/testing.md create mode 100644 .claude/skills/workflows/references/guides/workflows-exception.mdx create mode 100644 .claude/skills/workflows/references/guides/workflows-plugins.mdx create mode 100644 .claude/skills/workflows/references/guides/workflows.mdx create mode 100644 .claude/skills/workflows/references/pipeline_pattern.md create mode 100644 .claude/skills/workflows/references/workflow_testing.md create mode 100644 .claude/skills/workflows/scripts/test_workflow.py diff --git a/.claude/skills/workflows/SKILL.md b/.claude/skills/workflows/SKILL.md new file mode 100644 index 000000000..2bafb0147 --- /dev/null +++ b/.claude/skills/workflows/SKILL.md @@ -0,0 +1,105 @@ +--- +name: workflows +description: Framework for building durable workflows with orchestrated activities, used for background jobs, multi-step pipelines, scheduled tasks, LLM agents, or any process requiring fault tolerance, retries, and long-running execution. This skill provides comprehensive documentation and guidance for working with the Mistral Workflows framework. +--- + +# Workflows Documentation + +This skill provides comprehensive documentation and guidance for the Mistral Workflows framework, which is designed for building durable, fault-tolerant workflows with orchestrated activities. + +## About Workflows + +Mistral Workflows is an orchestration control plane that accelerates the development and reliable execution of complex, AI-driven workflows. Built on Temporal for fault-tolerant workflow execution, it combines a user-friendly API with a rich Python framework optimized for Mistral's AI services. + +## Documentation Structure + +The documentation is organized into several categories: + +### Getting Started + +- **[Introduction](references/getting-started/introduction.mdx)**: Overview of Mistral Workflows and its core architecture +- **[Value Proposition](references/getting-started/value-proposition.mdx)**: Why Mistral Workflows vs. raw Temporal — AI-specific features and DX improvements +- **[Installation](references/getting-started/installation.mdx)**: Guide to installing and setting up the Workflows framework (CLI scaffolding, optional deps) +- **[Core Concepts](references/getting-started/core-concepts.mdx)**: Workflows, activities, workers, executions vs runs +- **[Python SDK](references/getting-started/python-sdk.mdx)**: Documentation for the Python SDK and WorkflowsClient +- **[Your First Workflow](references/getting-started/your-first-workflow.mdx)**: Step-by-step guide to creating your first workflow + +### Guides + +- **[Workflows](references/guides/workflows.mdx)**: Creating workflows, determinism enforcement (sandbox), input types, timeouts, signals/queries/updates, child workflows, continue-as-new +- **[Activities](references/guides/activities.mdx)**: Timeouts, retries, heartbeats, local activities, sticky sessions, nested activities +- **[Workflows Exception Handling](references/guides/workflows-exception.mdx)**: WorkflowsException, ErrorCode enum, factory methods +- **[Error Codes](references/guides/error-codes.mdx)**: API error codes WF_1000-WF_1600 with HTTP status, description, and resolution +- **[Signals, Queries, and Updates](references/guides/signals-queries-updates.mdx)**: Workflow communication patterns with input validation +- **[Scheduling](references/guides/scheduling.mdx)**: Cron expressions, ScheduleDefinition, SchedulePolicy, overlap handling +- **[Dependency Injection](references/guides/dependency-injection.mdx)**: FastAPI-style Depends() pattern +- **[Streaming](references/guides/streaming.mdx)**: Task API, token streaming, progress updates +- **[Streaming Consumption](references/guides/streaming-consumption.mdx)**: WorkflowsClient.stream_events(), NATS subjects, SSE API +- **[Concurrency](references/guides/concurrency.mdx)**: execute_activities_in_parallel() with List/Chain/Offset executors +- **[Rate Limiting](references/guides/rate-limiting.mdx)**: Distributed rate limiting across workers +- **[Handling Large Data](references/guides/handling-large-data.mdx)**: OffloadableField, blob storage (S3/Azure/GCS) +- **[Payload Encoding](references/guides/payload-encoding.mdx)**: Payload offloading, AES-GCM encryption, key rotation +- **[Observability](references/guides/observability.mdx)**: OpenTelemetry traces, trace sampling +- **[Durable Agents](references/guides/durable-agents.mdx)**: Agent, Runner, RemoteSession/LocalSession, MCP, multi-agent handoffs +- **[Conversational Workflows](references/guides/assist-workflows.mdx)**: InteractiveWorkflow, HITL, ChatInput/FormInput, Canvas editing, Rich UI components, Tool UI states +- **[Local Execution](references/guides/local-execution.mdx)**: No-infra dev mode with Pydantic model params +- **[Limitations](references/guides/limitations.mdx)**: System constraints and limits +- **[Workflows Plugins](references/guides/workflows-plugins.mdx)**: Mistral AI plugin, Webhook plugin, Nuage plugin, custom plugins +- **[Deployment Patterns](references/guides/_deployment-patterns.mdx)**: Best practices for deploying workflows +- **[Migration v2 to v3](references/guides/migration-v2-to-v3.mdx)**: Breaking changes and upgrade steps from SDK v2 to v3 + +### Testing + +- **[Testing Workflows](references/guides/testing.md)**: Integration testing with `create_test_worker`, hang prevention, sandbox pitfalls + +**Quick-test script** — run any workflow in a local Temporal test environment with zero setup: +```bash +python .claude/skills/workflows/scripts/test_workflow.py --input '{"key": "value"}' [--timeout 30] +``` + +**Timeout policy for testing:** Use aggressive (short) timeouts to keep the feedback loop tight. A hanging test wastes more time than a false timeout. Defaults: + +| Context | Recommended timeout | When to increase | +|---|---|---| +| `--timeout` (quick-test script) | `15` seconds | Workflow makes multiple LLM calls or heavy I/O | +| `execution_timeout` (pytest) | `timedelta(seconds=10)` | Known long-running workflow | +| `asyncio.wait_for` (pytest) | `15` seconds | Should always be slightly above `execution_timeout` | + +If a workflow is known to be long-running (e.g. multi-step agent, large data processing), increase timeouts proportionally — but start short and only raise them when you see legitimate timeout failures, not preemptively. + +### Internal References + +These are additional patterns and utilities not covered in the official docs: + +- **[Execution IDs](references/execution_ids.md)**: Generate deterministic execution IDs for child workflows +- **[Pipeline Pattern](references/pipeline_pattern.md)**: Build multi-step workflows with declarative StepSpec definitions +- **[Workflow Testing](references/workflow_testing.md)**: Ensure workflow classes are properly registered in workers + +## When to Use This Skill + +Use this skill when you need to: + +1. **Build durable workflows**: Create long-running, fault-tolerant processes +2. **Orchestrate activities**: Coordinate multiple tasks and operations +3. **Handle background jobs**: Manage asynchronous processing and task queues +4. **Create multi-step pipelines**: Build complex workflows with multiple stages +5. **Schedule tasks**: Set up recurring or delayed execution of workflows +6. **Develop LLM agents**: Build durable AI agents with MCP tool support +7. **Build conversational workflows**: Create interactive workflows with HITL, forms, canvas, and rich UI +8. **Ensure fault tolerance**: Implement systems that can recover from failures automatically +9. **Stream events**: Real-time token streaming and progress updates via NATS + +## Key Features + +- **Fault tolerance**: Automatic recovery from failures and retries +- **Durable execution**: Workflows can run for extended periods (seconds to years) +- **Determinism enforcement**: Sandbox-based determinism with configurable enforcement +- **Rich Python framework**: Easy-to-use decorators and APIs (`mistralai.workflows`) +- **Built-in observability**: Deep integration with OpenTelemetry for monitoring +- **Streaming**: NATS-backed real-time token and progress streaming +- **Rate limiting**: Distributed rate limiting shared across workers +- **Dependency injection**: FastAPI-style Depends() pattern +- **Large payload handling**: OffloadableField with S3/Azure/GCS blob storage +- **Conversational workflows**: Interactive workflows with Le Chat integration, forms, canvas, and rich UI components +- **Durable agents**: AI agents with MCP support, multi-agent handoffs, and persistent state +- **Scalability**: Designed to handle complex, distributed applications diff --git a/.claude/skills/workflows/references/execution_ids.md b/.claude/skills/workflows/references/execution_ids.md new file mode 100644 index 000000000..ddcab81a7 --- /dev/null +++ b/.claude/skills/workflows/references/execution_ids.md @@ -0,0 +1,202 @@ +# Deterministic Child Workflow Execution IDs + +Generate reproducible execution IDs for child workflows to enable idempotency and prevent duplicate executions. + +## Overview + +When executing child workflows, providing a deterministic execution ID ensures: +- **Idempotency**: Re-running the parent workflow won't create duplicate child executions +- **Traceability**: Easy to correlate parent and child workflows +- **Replay safety**: Workflow replays use the same execution IDs + +## SDK Context + +The SDK's `execute_workflow` accepts an optional `execution_id` parameter. When omitted: +- **Inside a workflow**: a random child workflow ID is generated +- **Outside a workflow (client calls)**: The SDK auto-generates a 64-char hex ID via `generate_two_part_id()` using uuid5 + +For child workflows where idempotency matters, always provide a deterministic `execution_id`. + +## Implementation + +```python +def get_child_workflow_execution_id(task_name: str, case_id: str) -> str: + """Generate a deterministic execution ID for a child workflow. + + Args: + task_name: Name/type of the task being executed + case_id: Unique identifier for the case/entity being processed + + Returns: + A deterministic, unique execution ID + + Example: + >>> get_child_workflow_execution_id("validate-input", "case-123") + "case-123-validate-input" + """ + return f"{case_id}-{task_name}" +``` + +## Usage + +### Basic Usage + +```python +import mistralai.workflows as workflows +from .utils import get_child_workflow_execution_id + +@workflows.workflow.define(name="parent-workflow") +class ParentWorkflow: + + @workflows.workflow.entrypoint + async def run(self, params: ParentParams) -> Result: + case_id = params.case_id + + # Execute child workflow with deterministic ID + validation_result = await workflows.workflow.execute_workflow( + ValidateInputWorkflow, + ValidateParams(data=params.data), + execution_id=get_child_workflow_execution_id( + task_name="validate-input", + case_id=case_id + ), + ) + + # Another child workflow with fire-and-forget + handle = await workflows.workflow.execute_workflow( + ProcessDataWorkflow, + ProcessParams(data=validation_result.data), + execution_id=get_child_workflow_execution_id( + task_name="process-data", + case_id=case_id + ), + wait=False, # returns ChildWorkflowHandle immediately + ) + + # Can await the handle later + process_result = await handle + + return Result(output=process_result.output) +``` + +### In Pipeline Pattern + +```python +async def run_task_queue( + self, + input_object: T, + ctx: Context, + steps: list[StepSpec], + case_id: str +) -> T: + for step in steps: + step_label = step.task.task_type.name.lower() + + # Generate deterministic execution ID + execution_id = get_child_workflow_execution_id( + task_name=step_label, + case_id=case_id + ) + + params = step.task.make_params(input_object, ctx, execution_id, self.process_type) + + input_object = await workflows.workflow.execute_workflow( + step.task.workflow_cls, + params, + execution_id=execution_id + ) + + return input_object +``` + +## Extended Patterns + +### Including Additional Context + +For workflows that process items within a case: + +```python +def get_child_workflow_execution_id( + task_name: str, + case_id: str, + item_id: str | None = None +) -> str: + """Generate execution ID with optional item-level granularity.""" + if item_id: + return f"{case_id}-{task_name}-{item_id}" + return f"{case_id}-{task_name}" +``` + +Usage: + +```python +# Processing multiple items within a case +for item in items: + await workflows.workflow.execute_workflow( + ProcessItemWorkflow, + ProcessItemParams(item=item), + execution_id=get_child_workflow_execution_id( + task_name="process-item", + case_id=case_id, + item_id=item.id, + ), + ) +``` + +## `execute_workflow` Full Signature + +For reference, the SDK's `execute_workflow` accepts: + +```python +await workflows.workflow.execute_workflow( + workflow=MyWorkflow, # workflow class (decorated with @workflow.define) + params=MyParams(...), # Pydantic BaseModel + execution_timeout=timedelta(hours=1), # max runtime (default: 1h) + execution_id="my-deterministic-id", # optional; auto-generated if None + wait=True, # True → await result; False → return ChildWorkflowHandle + parent_close_policy=None, # TERMINATE (wait=True) or ABANDON (wait=False) by default +) +``` + +## Why Deterministic IDs Matter + +### Without Deterministic IDs + +```python +# BAD: Random/missing execution IDs +await workflows.workflow.execute_workflow( + ChildWorkflow, + params, + # No execution_id - system generates random one +) +``` + +Problems: +- Each replay creates new child workflow executions +- No way to correlate parent-child relationships +- Duplicate work if parent workflow is retried + +### With Deterministic IDs + +```python +# GOOD: Deterministic execution ID +await workflows.workflow.execute_workflow( + ChildWorkflow, + params, + execution_id=get_child_workflow_execution_id("child-task", case_id), +) +``` + +Benefits: +- Replays reuse existing child executions +- Clear parent-child correlation in traces +- Safe retries without duplicate work + +## Best Practices + +1. **Always provide execution IDs** for child workflows +2. **Use consistent naming**: lowercase, hyphenated task names +3. **Include enough context**: case ID at minimum, item ID if processing collections +4. **Keep IDs readable**: they appear in logs and traces +5. **Don't include timestamps** unless you specifically want daily re-execution +6. **Use the same ID generation** across all child workflow calls in a parent diff --git a/.claude/skills/workflows/references/getting-started/core-concepts.mdx b/.claude/skills/workflows/references/getting-started/core-concepts.mdx new file mode 100644 index 000000000..757e464d4 --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/core-concepts.mdx @@ -0,0 +1,192 @@ +--- +id: core-concepts +title: Core Concepts +sidebar_position: 4 +--- + +# Core Concepts: Workflows, Activities & Workers + +Workflows provides a robust framework for building distributed applications through three key components that work together seamlessly. + +## 1. Workflows: The Brains of Your Application + +Workflows define the high-level business logic and coordination of your application. + +```python +@workflows.workflow.define(name="data_pipeline") +class DataProcessingWorkflow: + @workflows.workflow.entrypoint + async def run(self, raw_data: str) -> dict: + # Clean the data before parallel processing + cleaned_data_result = await clean_data(raw_data) + + # Run activities in parallel + analysis_result, transformed_data = await asyncio.gather( + analyze_data(cleaned_data_result), + transform_data(cleaned_data_result), + ) + + # For large-scale parallel processing, use the concurrency framework + # [Learn more about concurrency patterns](../guides/concurrency) + + return await generate_report(analysis_result, transformed_data) +``` + +Key characteristics: + +- Deterministic execution (same inputs → same outputs) +- Long-running (can execute for years with checkpointing) +- Stateful coordination +- Input/output limited to 2MB +- **A workflow will timeout if the delay between activities is more than 2 seconds** + +Workflows should contain only orchestration logic - they decide what needs to happen and in what order, but never perform actual work directly. + +## 2. Activities: The Workhorses + +Activities perform the actual work in your application. + +```python +@workflows.activity() +async def clean_data(raw_data: str) -> dict: + """Remove noise and normalize data format""" + # Implementation details... +``` + +Key characteristics: + +- Isolated execution in separate processes +- Automatic retries on failure +- Input/output limited to 2MB +- Idempotent by design (safe to retry) +- Focused on single responsibilities + +## 3. Workers: The Execution Engines + +Workers execute workflows and activities. They form the scalable execution layer. + +```python +async def main(): + await workflows.run_worker([ + DataProcessingWorkflow, + clean_data, + analyze_data, + transform_data, + generate_report + ]) +``` + +### Scaling with Multiple Workers + +When you need more throughput or want to test different code simultaneously, you can run multiple workers. Here are the common patterns: + +> **What is a task queue?** A task queue is a lightweight routing mechanism that determines which workers can execute which workflows and activities. Workers poll specific task queues for work, and workflows/activities are assigned to task queues when executed. [Learn more about task queues](https://docs.temporal.io/task-queue). + +```mermaid +graph TD + Queue[Task Queue] -->|Task 1| Worker1[Worker 1] + Queue -->|Task 2| Worker2[Worker 2] + Queue -->|Task 3| Worker3[Worker 3] +``` + +**Pattern 1: Horizontal Scaling (Same code, more capacity)** + +- Multiple workers running identical code +- Same namespace (same API key) +- Same task queue +- Tasks distributed automatically +- Use case: Production workloads needing more throughput + +```bash +## Worker 1, 2, 3... all identical +MISTRAL_API_KEY=prod_key uv run python my_workflow.py +``` + +**Pattern 2: Environment Separation (Dev/Staging/Prod)** + +- Different workspaces for different environments +- Each environment gets its own namespace +- Complete isolation between environments + +```bash +## Dev workspace +MISTRAL_API_KEY=dev_workspace_key uv run python my_workflow.py + +## Prod workspace +MISTRAL_API_KEY=prod_workspace_key uv run python my_workflow.py +``` + +**Pattern 3: Testing Different Code (Same workspace)** + +- Same workspace (same namespace) +- Different task queues for isolation +- Use case: Testing changes without affecting running workers + +```bash +## Stable version +MISTRAL_API_KEY=workspace_key uv run python my_workflow.py + +## Testing changes +TEMPORAL_TASK_QUEUE=testing MISTRAL_API_KEY=workspace_key uv run python my_workflow_v2.py +``` + +:::danger Critical: Avoid Worker Conflicts + +**Namespaces are automatically derived from your API key: `customer_id:workspace_id`** + +Workers using the same API key share the same namespace + +**What could happen:** + +- Version conflicts: Different code versions handling the same workflows +- Unpredictable behavior: Any worker can pick up any task +- Hard to debug: Can't trace which worker executed what + +**How to avoid conflicts:** + +- For different environments → Use separate workspaces (different namespaces) +- For testing changes → Use different task queues on the same workspace +- For scaling → Use same workspace, same task queue, same code + +::: + +## Complete Execution Flow + +1. Workflow triggered (API/schedule/manual) +2. Task added to queue +3. Available worker pulls task and executes workflow +4. When workflow hits activity: + - Activity task added to queue + - Any available worker pulls and executes it +5. Results flow back through the system + +## Key Differences + +| Component | Responsibility | Execution Location | Lifetime | Scaling Impact | +| --------- | -------------- | -------------------- | ------------ | ---------------------------------------- | +| Workflow | Orchestration | Worker process | Long-running | More workers = more concurrent workflows | +| Activity | Actual work | Any available worker | Short-lived | More workers = more parallel execution | +| Worker | Execution | Your infrastructure | Long-running | Horizontal scaling unit | + +## Executions vs Runs + +Understanding the distinction between **Executions** and **Runs** helps clarify the API behavior. + +### Execution + +An **Execution** represents a workflow invocation with a unique `execution_id`. When you call action endpoints like `GET /executions/{id}`, `POST /executions/{id}/terminate`, or `POST /executions/{id}/signals`, you interact with the **latest run** of that execution. + +### Run + +A **Run** is a single attempt within an execution. When a workflow is reset or retried, a new run is created for the same execution. Key points: + +- **1 Execution = 1+ Runs**: An execution can have multiple runs over its lifetime +- **Only 1 active run at a time**: For any execution, only one run can be active (RUNNING) at any moment +- **Action endpoints target latest run**: This is Temporal's default behavior and ensures you always interact with the current state + +### API Endpoints + +- `GET /v1/workflows/runs` - List all workflow runs (preferred) +- `GET /v1/workflows/executions` - **Deprecated** - Legacy alias for `/runs` +- `GET /v1/workflows/executions/{id}` - Get execution details (targets latest run) +- Action endpoints (`/terminate`, `/signals`, `/queries`, etc.) - Target latest run diff --git a/.claude/skills/workflows/references/getting-started/installation.mdx b/.claude/skills/workflows/references/getting-started/installation.mdx new file mode 100644 index 000000000..7f664a384 --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/installation.mdx @@ -0,0 +1,93 @@ +--- +id: installation +title: Installation Guide +sidebar_position: 2 +--- + +# Installation + +This guide will walk you through setting up Workflows and verifying your installation. + +## Prerequisites + +Before installing the Workflows SDK, ensure you have: + +1. Python 3.12 or later installed +2. [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager installed + +## Install Workflows + +Install the Workflows package from PyPI using uv: + +```bash +uv add mistralai-workflows +``` + +This will create a virtual environment (if one doesn't exist) and install Workflows along with its core dependencies. + +## Scaffold a Project with the CLI + +If you want to start from a ready-to-run Python project instead of wiring the SDK manually, use `mistralai-workflows-cli`: + +```bash +uvx mistralai-workflows-cli setup +``` + +This scaffolds a ready-to-run Python project with the Workflows SDK already configured, a minimal example workflow, and helper commands to register and execute workflows. + +The generated project also includes an embedded Workflows skill at `.agents/skills/workflows/SKILL.md`, so coding agents already have the project-specific guidance needed to work with the SDK. + +### Installing with Optional Dependencies + +The Workflows SDK provides optional extras for specific features. Install only what you need: + +#### Cloud Storage Providers + +For handling large payloads with [payload offloading](../../appendices/payload_encoding#payload-offloading): + +```bash +## AWS S3 support +uv add "mistralai-workflows[s3]" + +## Azure Blob Storage support +uv add "mistralai-workflows[azure]" + +## Google Cloud Storage support +uv add "mistralai-workflows[gcs]" + +## All storage providers +uv add "mistralai-workflows[storage]" +``` + +#### Mistral AI Integration + +For using Mistral AI models in your workflows: + +```bash +uv add "mistralai-workflows[mistralai]" +``` + +#### All Optional Features + +To install all optional dependencies: + +```bash +uv add "mistralai-workflows[all]" +``` + +## Verify Installation + +To verify your installation was successful, run the following command: + +```bash +uv run python -c "import mistralai.workflows; print('Workflows is installed successfully!')" +``` + +If you encounter any errors during this step, please check that your Python environment is properly configured. + +## Next Steps + +Now that you have Workflows installed, you can proceed to: + +- [Your First Workflow](your-first-workflow) - Create and run your first workflow +- [Core Concepts](core-concepts) - Understand the fundamental concepts diff --git a/.claude/skills/workflows/references/getting-started/introduction.mdx b/.claude/skills/workflows/references/getting-started/introduction.mdx new file mode 100644 index 000000000..259def5cc --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/introduction.mdx @@ -0,0 +1,30 @@ +--- +id: introduction +title: Welcome to the Workflows documentation +sidebar_position: 1.1 +--- + +Mistral Workflows is an orchestration control plane designed to accelerate the development and reliable execution of complex, AI-driven workflows. Built on Temporal for fault-tolerant workflow execution, Workflows combines a user-friendly API with a rich Python framework optimized for Mistral's AI services. + +# What is Workflows? + +Workflows addresses the complexity of building, managing, and scaling multi-step AI processes reliably. It provides a structured environment for defining, executing, and monitoring workflows—from simple sequences to complex, stateful processes—ensuring completion even with transient failures. + +### Core Architecture + +- **Worker Framework** (`workflows/worker`): Python decorators for building and running Temporal workflows with minimal boilerplate +- **Python SDK** (`mistralai.Mistral`): Convenient client library for programmatic interaction with the Workflows API via `client.workflows.*` +- **Built-in Observability**: Deep OpenTelemetry integration linking Temporal executions with distributed traces + +### Key Features + +- **Rapid Development**: Python-first framework with intuitive decorators and pre-built components +- **Reliable Execution**: Leverages Temporal's battle-tested fault tolerance, automatic retries, and state management +- **Mistral-Native**: Optimized for orchestrating workflows utilizing Mistral's AI models and services +- **Scalable & Flexible**: Handles complex, long-running processes with multiple deployment patterns + +## Why Choose Workflows? + +Modern AI applications involve multi-step processes that are complex to build reliably. Integrating services, handling retries, ensuring observability, and managing long-running tasks quickly becomes an engineering challenge. Workflows provides the infrastructure to focus on your AI workflow logic rather than orchestration complexity. + +📖 **For a detailed comparison with raw Temporal and a full feature list, see [Value Proposition](./value-proposition).** diff --git a/.claude/skills/workflows/references/getting-started/python-sdk.mdx b/.claude/skills/workflows/references/getting-started/python-sdk.mdx new file mode 100644 index 000000000..5550b8f31 --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/python-sdk.mdx @@ -0,0 +1,257 @@ +--- +id: python-sdk +title: Python SDK +sidebar_position: 5 +--- + +# Python SDK Guide + +The Workflows Python SDK provides a convenient way to interact with the Workflows platform programmatically, allowing you to execute workflows, monitor their progress, and manage workflow definitions. + +## Prerequisites + +Before using the SDK, ensure you have: + +1. Followed the [installation instructions](./installation) to set up Workflows +2. Completed the [your first workflow](./your-first-workflow) guide to run a worker with your first workflow + +## Getting Started + +### Basic Setup + +Initialize the client with your API credentials and namespace: + +```python +from mistralai.workflows.client import get_mistral_client + +## Initialize client +client = get_mistral_client( + server_url="https://api.mistral.ai", + api_key="your-api-key", # Should match your worker's `MISTRAL_API_KEY` +) +``` + +## Core Workflow Operations + +### 1. Executing Workflows + +Start a workflow execution with optional input data and custom execution ID: + +```python +from pydantic import BaseModel + +class InputData(BaseModel): + name: str + +execution_id = "custom-id-123" +execution = await client.workflows.execute_workflow_async( + workflow_identifier="simple_example_workflow", + input=InputData(name="MyName").model_dump(mode="json"), + execution_id=execution_id # Optional +) +``` + +### 2. Execute and Wait for Results + +For workflows where you need immediate results, use the synchronous execution method: + +```python +from pydantic import BaseModel + +class InputData(BaseModel): + name: str + +result = await client.workflows.execute_workflow_async( + workflow_identifier="simple_example_workflow", + input=InputData(name="MyName").model_dump(mode="json"), +) +``` + +### 3. Checking Execution Status + +Monitor the progress of your workflow executions: + +```python +status = await client.workflows.executions.get_workflow_execution_async(execution_id=execution_id) +print(f"Current status: {status.status}") +``` + +## Advanced Workflow Interactions + +### Signaling Workflows + +Send signals to running workflows to trigger specific behaviors: + +```python +from pydantic import BaseModel + +class SignalData(BaseModel): + new_value: int + +await client.workflows.executions.signal_workflow_execution_async( + execution_id="your-execution-id", # Execution id of the running workflow with signal method + name="update_config", # Name of the signal method + input=SignalData(new_value=42).model_dump(mode="json") +) +``` + +### Querying Workflow State + +Retrieve information from running workflows without modifying their state: + +```python +response = await client.workflows.executions.query_workflow_execution_async( + execution_id="your-execution-id", # Execution id of the running workflow with query method + name="get_status" # Name of the query method +) +print(response.result) +``` + +### Updating Running Workflows + +Modify workflow behavior during execution: + +```python +from pydantic import BaseModel + +class UpdateData(BaseModel): + timeout: int + +response = await client.workflows.executions.update_workflow_execution_async( + execution_id="your-execution-id", + name="change_settings", + input=UpdateData(timeout=300).model_dump(mode="json") +) +``` + +### Terminating Running Workflows + +Terminate a workflow during execution: + +```python +await client.workflows.executions.terminate_workflow_execution_async(execution_id=execution_id) +``` + +## Workflow Management + +### Listing Available Workflows + +Discover all registered workflows in your namespace: + +```python +response = await client.workflows.get_workflows_async() +print(f"These are all the available workflows: {response.workflows}") +``` + +### Retrieving Workflow Definitions + +Get detailed information about a specific workflow: + +```python +response = await client.workflows.get_workflow_async(workflow_identifier="simple_example_workflow") +print(f"The definition of this workflow is: {response.workflow}.") +``` + +## Execution Monitoring + +### Viewing Execution Traces + +Get detailed OpenTelemetry traces for debugging and analysis: + +```python +trace = await client.workflows.executions.get_workflow_execution_trace_otel_async(execution_id) +``` + +### Getting Trace Summaries + +Retrieve condensed information about workflow executions: + +```python +summary = await client.workflows.executions.get_workflow_execution_trace_summary_async(execution_id) +``` + +### Accessing Trace Events + +Examine individual events in a workflow execution: + +```python +events = await client.workflows.executions.get_workflow_execution_trace_events_async( + execution_id, + merge_same_id_events=True # Combines events with the same ID +) +``` + +## Scheduling Workflows + +### Creating Recurring Executions + +Set up workflows to run on a schedule: + +```python +from mistralai.workflows.models import ScheduleDefinition +schedule_id = "daily-name-schedule" +schedule = await client.workflows.schedules.schedule_workflow_async( + workflow_identifier="simple_example_workflow", + schedule=ScheduleDefinition( + input={"name": "MyName"}, + cron_expressions=["0 0 * * *"] # Runs daily at midnight + ), + schedule_id=schedule_id, +) +``` + +### Managing Schedules + +View and manage all your scheduled workflows: + +```python +## List all schedules +response = await client.workflows.schedules.get_schedules_async() +print(f"Here are all the defined schedules: {response.schedules}") +## Remove a schedule +await client.workflows.schedules.unschedule_workflow_async(schedule_id=schedule_id) +``` + +## Complete Example + +Here's a complete example showing workflow execution and result retrieval: + +```python +from dotenv import load_dotenv + +load_dotenv() + +async def main() -> None: + from mistralai.workflows.client import get_mistral_client + from pydantic import BaseModel + + class InputData(BaseModel): + report_type: str + + client = get_mistral_client( + server_url="https://api.mistral.ai", + api_key=os.environ["MISTRAL_API_KEY"], + ) + + # Execute workflow + result = await client.workflows.execute_workflow_async( + workflow_identifier="report_generator", + input=InputData(report_type="weekly").model_dump(mode="json"), + ) + + print(f"Workflow completed with result: {result}") + +if __name__ == "__main__": + + import asyncio + asyncio.run(main()) +``` + +## Next Steps + +Explore these related topics to deepen your understanding: + +- [Workflows Guide](../guides/workflows) - Learn about workflow fundamentals and design patterns +- [Activities Guide](../guides/activities) - Learn about activity functions and their usage +- [Scheduling](../guides/scheduling) - Advanced scheduling techniques and cron expressions +- [Signals, Queries & Updates](../guides/signals-queries-updates) - Advanced workflow interaction patterns diff --git a/.claude/skills/workflows/references/getting-started/value-proposition.mdx b/.claude/skills/workflows/references/getting-started/value-proposition.mdx new file mode 100644 index 000000000..73673a5c2 --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/value-proposition.mdx @@ -0,0 +1,333 @@ +--- +id: value-proposition +title: "Value Proposition: Why Mistral Workflows?" +sidebar_position: 2 +--- + +# Mistral Workflows: Value Proposition + +Mistral Workflows (SDK + control plane) is our orchestration platform for building production grade AI workflows. It combines the reliability of Temporal with a developer-friendly Python SDK and native Mistral AI integrations. + +--- + +## Why We Built This (vs. Using Raw Temporal) + +Temporal is excellent infrastructure. We extend it with AI-specific features: + +| What Temporal Provides | What We Add | +|------------------------|-------------| +| Verbose SDK patterns | **Simpler DX**: direct activity calls, config on decorators, auto-schemas | +| No LLM-specific features | Native Mistral AI plugin (Agents, Tools, Sessions) | +| **No streaming** | **NATS-backed real-time event/token streaming** | +| Temporal UI (cluster-level view) | Native integration with AI Studio + Le Chat | +| No chat/interaction primitives | Primitives for building interactive chat experiences | +| No builtin interactivity | **HITL (Human-in-the-Loop)** interactive workflows with `wait_for_input()` | +| Basic tracing support | Pre-configured OpenTelemetry + Tempo + Grafana | +| Manual worker registration | Auto-registration with Workflows control plane | +| Temporal gRPC API | **Higher-level REST API** for workflows, executions, events, schedules | +| Manual namespace management | **Automatic namespace isolation** per org+workspace (`{org_id}:{workspace_id}`) | + +--- + +## Core Temporal Features We Leverage + +Temporal provides battle-tested distributed systems primitives: + +- **Fault Tolerance**: Workflows survive process crashes, network failures, infrastructure outages +- **Exactly-Once Execution**: Activities execute exactly once despite retries +- **Durability**: Full execution history persisted; workflows can run for months +- **Deterministic Replay**: Workflow state reconstructed from event history +- **Automatic Retries**: Configurable retry policies with exponential backoff +- **Signals & Queries**: External interaction with running workflows +- **Child Workflows**: Compose complex workflows from smaller units +- **Schedules**: Cron-like scheduled executions + +--- + +## Simpler Developer Experience + +Our SDK reduces boilerplate while adding capabilities. Here's why it's simpler: + +### 1. Direct Activity Calls + +**Raw Temporal:** Every activity call requires `execute_activity()` with explicit timeout/retry: +```python +result = await workflow.execute_activity( + my_activity, name, + start_to_close_timeout=timedelta(minutes=5), + retry_policy=RetryPolicy(maximum_attempts=3), +) +``` + +**Our SDK:** Call activities directly — config lives on the decorator: +```python +@workflows.activity(start_to_close_timeout=timedelta(minutes=5), retry_policy_max_attempts=3) +async def my_activity(name: str) -> str: ... + +## In workflow: +result = await my_activity(name) # Just call it +``` + +### 2. Configuration on Decorators + +| Raw Temporal | Our SDK | +|--------------|---------| +| `@activity.defn` (no config) | `@activity(timeout=..., retry=..., rate_limit=..., display_name=...)` | +| `@workflow.defn` (no config) | `@workflow.define(name=..., display_name=..., description=...)` | +| Config at every call site | Config once on the decorator | + +### 3. Auto-Generated Schemas + +| Raw Temporal | Our SDK | +|--------------|---------| +| No schema export | **Auto-generates JSON schemas** from signatures | +| Manual UI integration | Schemas registered → AI Studio input forms | + +### 4. Built-in Dependency Injection + +```python +@workflows.activity() +async def my_activity( + db: Database = Depends(get_database), # Injected + client: MistralClient = Depends(get_client) # Injected +) -> Result: ... +``` + +📖 **Full comparison:** See code examples in [Workflows Guide](../guides/workflows) and [Activities Guide](../guides/activities) + +--- + +## Complete Feature List (What We Add on Top of Temporal) + +### 1. Real-Time Streaming + +Temporal workflows return results only when complete—no way to stream intermediate outputs. + +- Token-by-token LLM output in AI Studio and Le Chat +- Progress updates via observable tasks +- 15-minute message persistence with replay +- NATS JetStream → Abraxas SSE → Client + +📖 **Docs:** [Streaming Guide](../guides/streaming) | [Streaming Architecture](../appendices/streaming) + +### 2. Dependency Injection + +FastAPI-style `Depends()` pattern for activities: +- Sync/async functions, context managers, generators +- Single instance per worker (shared across executions) +- Automatic lifecycle management + +📖 **Docs:** [Dependency Injection Guide](../guides/dependency-injection) + +### 3. Distributed Rate Limiting + +- Rate limits shared across **all workers** in the workspace +- Per-activity or shared via `key` parameter +- Prevents API quota exhaustion + +📖 **Docs:** [Rate Limiting Guide](../guides/rate-limiting) + +### 4. Concurrency Framework + +`execute_activities_in_parallel()` with 3 patterns: +- **List Executor**: Known collection of items +- **Chain Executor**: Token-based pagination (S3, DynamoDB) +- **Offset Pagination**: Index-based pagination + +Includes automatic continue-as-new for large datasets. + +📖 **Docs:** [Concurrency Guide](../guides/concurrency) + +### 5. Large Payload Handling + +Temporal has a 2MB limit and supports custom codecs for encryption. We add: +- **Built-in Payload Offloading**: Auto-offload to S3/GCS/Azure (Temporal requires third-party solutions) +- **Offloadable Fields**: Per-field offloading in activities +- **Pre-configured Encryption**: AES-GCM + key rotation out of the box + +📖 **Docs:** [Handling Large Data](../guides/handling-large-data) | [Payload Encoding](../appendices/payload-encoding) + +### 6. Observability / Task System + +- Observable `task()` context manager with state updates +- Automatic lifecycle events (started, completed, failed) +- OpenTelemetry tracing across workflows and activities +- Tempo integration for distributed trace queries +- Prometheus metrics + Grafana dashboards + +📖 **Docs:** [Observability Guide](../guides/observability) | [Streaming Guide](../guides/streaming) + +### 7. UI Metadata & Discovery + +- `workflow_display_name`, `workflow_description` for AI Studio +- Auto-generated JSON schemas → UI input forms +- `available_in_chat_assistant` flag for Le Chat + +📖 **Docs:** [Workflows Guide](../guides/workflows) + +### 8. Durable Agents (Mistral Plugin) + +LLM agents with Temporal's durability guarantees: +- Activities as agent tools +- Multi-agent handoffs +- MCP support (stdio/SSE) +- Built-in tools: WebSearch, CodeInterpreter, ImageGeneration, DocumentLibrary + +📖 **Docs:** [Durable Agents Guide](../guides/durable-agents) + +### 9. HITL (Human-in-the-Loop) Interactive Workflows + +Temporal workflows run autonomously with no built-in support for human interaction. We add **interactive workflow primitives** for pausing workflows and collecting human input: + +**Core Capability:** +- **`InteractiveWorkflow` base class**: Workflows that can pause and resume based on human input +- **`wait_for_input()`**: Pause workflow execution until user provides input +- **Durability guarantee**: Workflows survive crashes while waiting for input (full Temporal durability) + +📖 **Docs:** [Conversational Workflows Guide](../guides/assist-workflows) + +### 10. Local Execution Mode + +Temporal has a test environment, but requires spinning up a test server. We add: +- Run workflows/activities as plain Python functions (no server) +- Automatic retries via tenacity +- Dependency injection works locally +- **Limitations**: No durability, streaming, or human-in-the-loop + +📖 **Docs:** [Local Execution Guide](../guides/local-execution) + +### 11. Simplified Worker Versioning (Experimental) + +Temporal has worker versioning with PINNED behavior. We simplify it: +- Auto-registration as current version on startup +- No manual version management required +- Temporal Worker Controller integration for Kubernetes + +📖 **Docs:** [Worker Versioning](../appendices/worker-versioning) + +### 12. Simplified Sticky Sessions + +Temporal has Sessions for activity affinity. We simplify the API: +- Single decorator flag: `sticky_to_worker=True` +- No manual session management +- Works with dependency injection for state reuse + +📖 **Docs:** [Activities Guide](../guides/activities#sticky-worker-sessions) + +### 13. Simplified Worker Setup + +- Single `run_worker()` call +- Auto-discovery of decorated activities +- Auto-registration with control plane + +📖 **Docs:** [Your First Workflow](./your-first-workflow) + +### 14. Higher-Level REST API + +| Endpoint | Purpose | +|----------|---------| +| `/v1/workflows` | Lifecycle management, registry | +| `/v1/executions` | Monitoring, control, history | +| `/v1/events` | Event querying, SSE streaming | +| `/v1/schedules` | Cron and one-time schedules | +| `/v1/workers` | Worker capability discovery | + +### 15. Namespace Isolation + +Multi-tenant isolation built on Temporal namespaces: + +- **Namespace Format**: `{customer_id}:{workspace_id}` — Each org+workspace combination gets a dedicated Temporal namespace +- **Complete isolation**: Workflows, activities, schedules, and workers are isolated per namespace +- **Automatic resolution**: SDK and API automatically resolve namespace from authenticated context +- **Access control**: Namespace-level authentication and authorization via Kong/Albe +- **Independent scaling**: Each namespace can scale workers independently +- **Shared workflows**: Special handling for workflows owned by reserved namespaces that can be called across workspaces +- **Easy management**: Create and manage organizations and workspaces directly from AI Studio + +This enables: +- **SaaS multi-tenancy**: Complete isolation between organizations and their workspaces +- **Team/project isolation**: Different workspaces within the same organization are isolated +- **Cross-workspace sharing**: Opt-in shared workflows for common utilities + +### 16. AI Studio Integration + +- Timeline visualization with zoom/pan +- Input forms from Pydantic schemas +- Live execution status, signal sending + +### 17. Conversational Workflows and Le Chat integration + +Native integration with Le Chat (Mistral's chat interface): + +**Workflow Invocation:** +- Workflows appear as callable assistants in Le Chat +- LLM can invoke workflows as tools during conversations +- Automatic parameter validation from workflow schemas + +**Available Inputs:** +- **`ChatInput`**: Free-form conversational input for approvals, decisions, or feedback +- **`FormInput`**: Structured forms with typed fields and validation + - `TextField` (with regex validation), `NumberField` (min/max constraints) + - `DateTimeField`, `SingleChoice` (dropdown/select) + - Custom UI rendering in Le Chat + +**Rich Outputs:** +- **`TodoList`**: Real-time progress tracking with step-by-step status updates + - Three states: `todo`, `in_progress`, `done` + - Context manager support for automatic status transitions +- **Canvas outputs**: Rich content rendering + - Markdown, code with syntax highlighting, mermaid diagrams + - SVG, HTML, React components, presentation slides +- **Streaming agent responses**: Token-by-token LLM output streamed in real-time via tRPC + +📖 **Docs:** [Conversational Workflows Guide](../guides/assist-workflows) + +--- + +## Coming Soon + +Check the Milestones page for our roadmap: +https://www.notion.so/mistralai/Workflows-Milestones-2996ba59a7fe80719b9dd731ecbd380e?source=copy_link + +--- + +## Architecture at a Glance + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Clients │ +│ MistralClient (SDK) │ AI Studio │ Le Chat │ HTTP │ +└──────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ API Gateway (Kong) │ +│ + Auth (Albe/JWT) │ +└──────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────▼──────────────────────────────────────┐ +│ Workflows Control Plane (FastAPI) │ +│ /workflows /executions /events /schedules /workers │ +└───────┬─────────────────────────────────────────────────────────┘ + │ + ├──────────────────┬──────────────────┬───────────────────┐ + ▼ ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌────────────┐ +│ Temporal │ │ PostgreSQL │ │ NATS │ │ Tempo │ +│ Server │ │ (state) │ │ (streaming) │ │ (traces) │ +└───────────────┘ └───────────────┘ └───────────────┘ └────────────┘ + ▲ + │ +┌───────┴─────────────────────────────────────────────────────────┐ +│ Workflow Workers │ +│ @workflow.define │ @activity │ Mistral Plugin │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Next Steps + +- [Core Concepts](./core-concepts) — Understand workflows, activities, and workers +- [Your First Workflow](./your-first-workflow) — Build and run a workflow +- [Workflows Guide](../guides/workflows) — Deep dive into workflow patterns +- [Activities Guide](../guides/activities) — Learn about activity best practices diff --git a/.claude/skills/workflows/references/getting-started/your-first-workflow.mdx b/.claude/skills/workflows/references/getting-started/your-first-workflow.mdx new file mode 100644 index 000000000..d40130012 --- /dev/null +++ b/.claude/skills/workflows/references/getting-started/your-first-workflow.mdx @@ -0,0 +1,93 @@ +--- +id: your-first-workflow +title: Your First Workflow +sidebar_position: 3 +--- + +# Your First Workflow + +We'll walk through creating a simple workflow that executes a single activity. + +## Prerequisites + +First, follow the [installation instructions](./installation) to set up Workflows. +Before running your workflow, you'll need to: + +1. Connect to the console with your Mistral account +2. **Create a dedicated workspace** in the [Mistral Console](https://console.mistral.ai/) (requires admin permissions - if you're not an admin, ask your admin to create one for you) - this gives you an isolated namespace where you can safely run workers without conflicts +3. Create a new API key in that workspace (the namespace is automatically derived as `customer_id:workspace_id` from your API key) +4. Create a `.env` file with the following content: + +```bash +MISTRAL_API_KEY= +DEPLOYMENT_NAME= +``` + +## Step 1: Define Your Workflow and Worker + +Create a file `my_workflow.py` + +```python +import mistralai.workflows as workflows + +## Activities are async functions that can take any JSON-serializable types +@workflows.activity() +async def hello_world(name: str) -> dict: + """Activities are the building blocks of workflows. + 1. It's the place where you do the actual work + (e.g. call an API, process data, CPU intensive tasks, etc.) + 2. It must be async + 3. Parameters can be any JSON-serializable type (str, int, dict, list, etc.) + """ + return {"message": f"Hello, {name}!"} + +## Workflows orchestrate activities +@workflows.workflow.define(name="simple_example_workflow") +class SimpleExampleWorkflow: + @workflows.workflow.entrypoint + async def run(self, name: str) -> dict: + """Workflow entry point. + 1. It coordinate multiple activities + 2. It may need to wait for external events + """ + return await hello_world(name) + +## The Worker runs your workflows and activities +async def main() -> None: + await workflows.run_worker([SimpleExampleWorkflow]) + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) +``` + +## Step 2: Run Your Worker with the `simple_example_workflow` Workflow + +```bash +uv run python my_workflow.py +``` + +The worker will start, connect to the Mistral API, and register your workflow and wait for any task to do. +At that point you should already see your workflow registered in [AI studio](https://console.mistral.ai/build/workflows). + +Next step is now execute your workflow. + +## Step 3: Trigger execution of Your Workflow + +1. Visit [https://console.mistral.ai/](https://console.mistral.ai/) (make sure you are located in the same workspace than the one you created your API key in) +1. Click `Workflows` in the left sidebar +1. Select `simple_example_workflow` +1. Launch the workflow using the `Start Workflow` button and the input `{"name": }` +1. Find your execution in the `Executions` list +1. Make sure the workflow ran until the end by checking if the output is correct, like in this example: ![expected_workflow_output](expected_workflow_output.png) + +## Multi-Worker Setup + +For scaling patterns and running multiple workers, see [Core Concepts - Scaling with Multiple Workers](./core-concepts#scaling-with-multiple-workers). + +## Next Steps + +- [Core Concepts](core-concepts) - Understand the fundamentals +- [Workflows](../guides/workflows) - Deep dive in workflows +- [Activities](../guides/activities) - Deep dive in activities diff --git a/.claude/skills/workflows/references/guides/_deployment-patterns.mdx b/.claude/skills/workflows/references/guides/_deployment-patterns.mdx new file mode 100644 index 000000000..4b788aac9 --- /dev/null +++ b/.claude/skills/workflows/references/guides/_deployment-patterns.mdx @@ -0,0 +1,78 @@ +--- +id: deployment-patterns +title: Deployment Patterns +sidebar_position: 4 +--- + +*Four deployment strategies for different environments and use cases* + +# Overview +- Four distinct deployment patterns +- Kubernetes-native with Helm charts +- Local development options +- Hybrid cloud strategies + +## 1. Remote Fully (Production) +- **Components**: Temporal + Workflows API + Workers all on Kubernetes +- **Helm charts**: mistral-temporal, mistralai-workflows, mistralai-workflows-worker +- **Use case**: Production deployments, full observability, auto-scaling +- **Benefits**: High availability, resource isolation, operational visibility + +## 2. Hybrid (Development/Testing) +- **Components**: Temporal + Workflows API on Kubernetes, Workers local/elsewhere +- **Use case**: Development against production data, distributed teams +- **Benefits**: Local debugging, production-like environment, flexible scaling + +## 3. Local Passthru (Development) +- **Components**: Direct activity execution without Temporal orchestration +- **Implementation**: Activities detect `not temporalio.workflow.in_workflow()` and execute directly +- **Use case**: Fast iteration, debugging, unit testing +- **Benefits**: No infrastructure overhead, immediate execution, debugging-friendly + +## 4. Local with Temporal (Development) +- **Components**: `temporal server start-dev` + local API + local workers +- **Use case**: Full-stack local development, integration testing +- **Benefits**: Complete workflow testing, offline development, learning + +## Kubernetes Deployment + +### Helm Chart Structure +- mistral-ai-suite umbrella chart +- `global.mistral.apps.workflows.enabled` flag +- Component dependencies and ordering +- Configuration management + +### Configuration Management +- Environment-specific values +- Secret management strategies +- Resource limits and requests +- Scaling policies + +### Monitoring and Observability +- OpenTelemetry integration +- Grafana dashboards +- Alerting strategies +- Log aggregation + +## Local Development Setup + +### Prerequisites and Installation +- uv and Python environment setup +- Temporal server installation +- Development configuration + +### Switching Between Patterns +- Configuration changes for different patterns +- Environment variable management +- Testing strategies per pattern + +## Production Considerations +- Security and authentication +- Scaling worker pools +- Resource management +- Disaster recovery +- Multi-region deployments + +## Next Steps +- [Observability](observability) - Monitor your deployments +- [Configuration Reference](../appendices/configuration-reference) - All config options \ No newline at end of file diff --git a/.claude/skills/workflows/references/guides/activities.mdx b/.claude/skills/workflows/references/guides/activities.mdx new file mode 100644 index 000000000..bb2996331 --- /dev/null +++ b/.claude/skills/workflows/references/guides/activities.mdx @@ -0,0 +1,373 @@ +--- +id: activities +title: Activities +sidebar_position: 2 +--- + +# Activities: The Workhorses of Your Application + +Activities perform the actual work in your application while workflows coordinate them. + +## What is an Activity? + +An activity is a unit of work that performs actual computations, API calls, or other operations. Key characteristics: + +- Must be idempotent (safe to retry) +- Execute in isolated processes +- Have automatic retry mechanisms +- Accept any JSON-serializable types as inputs/outputs (str, int, dict, list, etc.) +- Input/output limited to 2MB + +> **Security Warning:** Do not pass sensitive data (API keys, passwords, PII, tokens) as activity inputs or outputs. +> All activity parameters and return values are persisted in the event history in plaintext unless +> [payload encryption](payload-encoding) is explicitly configured. Treat all inputs/outputs as visible +> to anyone with access to the namespace. + +## Defining an Activity + +Basic activity structure: + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def my_activity(input_data: str, count: int = 1) -> dict: + """Activity implementation""" + # Perform work here + return {"result": input_data, "processed_count": count} +``` + +:::warning +**All activity parameters and return types must have type annotations.** An activity missing any type hint on its arguments or return value will fail validation at registration time. Always provide explicit types for every parameter and the return type. + +```python +## ❌ Invalid - missing type annotations +@workflows.activity() +async def bad_activity(data, count=1): + return {"result": data} + +## ✅ Valid - all parameters and return type are annotated +@workflows.activity() +async def good_activity(data: str, count: int = 1) -> dict: + return {"result": data} +``` +::: + +## Core Activity Features + +### 1. Activity Naming + +Customize activity identification with: + +```python +@workflows.activity( + name="custom_activity_name", # Used for registration and execution + display_name="User-Friendly Name" # Used in logs and observability tools +) +``` + +[Learn more about observability and naming](#) + +### 2. Timeouts + +Configure execution time limits to prevent runaway activities: + +```python +from datetime import datetime + +@workflows.activity( + start_to_close_timeout=datetime.timedelta(minutes=10) +) +``` + +[Learn more about timeouts](#) + +### 3. Retry Policies + +Automatic retries with configurable policies to handle transient failures: + +```python +@workflows.activity( + retry_policy_max_attempts=5, + retry_policy_backoff_coefficient=2.0 +) +``` + +[Learn more about retry policies](#) + +### 4. Worker Stickiness + +Execute activities on the same worker for performance optimization: + +```python +@workflows.activity( + sticky_to_worker=True +) +``` + +See [Sticky Worker Sessions](#sticky-worker-sessions) for detailed documentation + +### 5. Heartbeat Timeout + +Detect stuck activities quickly by requiring periodic heartbeat signals: + +```python +from datetime import timedelta +from temporalio import activity as temporal_activity + +@workflows.activity( + start_to_close_timeout=timedelta(minutes=30), + heartbeat_timeout=timedelta(seconds=30) +) +async def long_running_task(items: list[str]) -> dict: + results = [] + for i, item in enumerate(items): + result = await process_item(item) + results.append(result) + # Report progress to prevent timeout + temporal_activity.heartbeat({"processed": i + 1, "total": len(items)}) + return {"results": results} +``` + +When `heartbeat_timeout` is set, activities must call `temporal_activity.heartbeat()` periodically. If no heartbeat is received within the timeout, Temporal considers the activity failed and triggers a retry. This enables fast detection of stuck activities without waiting for the full `start_to_close_timeout`. + +**Note:** Not supported for local activities. + +## Activity vs Workflow + +| Feature | Activity | Workflow | +| ----------- | ------------------ | -------------------- | +| Duration | Seconds to minutes | Seconds to years | +| State | Stateless | Stateful | +| Retries | Automatic | Manual recovery | +| Parallelism | Single operation | Complex coordination | + +## Granularity and Failure Handling + +Activities should be designed to be as granular as possible. This means breaking down complex tasks into smaller, manageable activities. However, each activity should encapsulate all the logic that is susceptible to failure. This granular approach has several benefits: + +- **Isolation of Failures**: Smaller activities make it easier to isolate and handle failures. If a failure occurs, only the affected activity needs to be retried. +- **Easier Debugging**: Granular activities make it easier to identify the exact point of failure and debug issues. +- **Better Retry Mechanisms**: Since each activity is idempotent and can be retried independently, granular activities ensure that only the failed part of the process is retried, saving time and resources. + +For example, if you have a process that involves fetching data from an API, processing that data, and then storing it in a database, you might define three separate activities: + +1. Fetch data from API +2. Process the data +3. Store the data in the database + +## Nested Activities + +When designing activities, it's important to understand how state management works, especially when activities are nested within each other. If an activity is encapsulated within another activity, only the parent activity will be considered for retries and state management. This means that the state will be saved before the nested activity, and only the parent activity will be retried in case of failure. + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def parent_activity(input_data: str) -> dict: + """Parent activity that encapsulates a nested activity.""" + # Some logic here + nested_result = await nested_activity(input_data) + return {"result_data": nested_result["result_data"]} + +@workflows.activity() +async def nested_activity(input_data: str) -> dict: + """Nested activity.""" + # Logic for the nested activity + return {"result_data": "processed_data"} +``` + +In this example, if nested_activity fails, the entire parent_activity will be retried, not just the nested activity. The state will be saved before the nested activity is called. + +## Local Activities + +For performance optimization, activities can run directly in the workflow worker process instead of being scheduled through Temporal's task queue. This is useful for fast operations (< 1 second) where scheduling overhead is significant. + +### When to Use + +**Good for:** + +- Quick computations, validations, data transformations +- High-volume scenarios where latency matters +- Operations completing in under 1 second + +**Avoid for:** + +- Long-running operations (> a few seconds) +- External API calls or I/O-heavy tasks +- Operations needing separate retry isolation or scaling + +### Basic Usage + +```python +from mistralai.workflows import run_activities_locally + +@workflows.activity() +async def validate_email(email: str) -> bool: + return "@" in email + +@workflows.workflow.define(name="user-workflow") +class UserWorkflow: + @workflows.workflow.entrypoint + async def execute(self, email: str) -> bool: + with run_activities_locally(): + result = await validate_email(email) + return result +``` + +### Mixed Execution + +Combine local and remote activities: + +```python +@workflows.workflow.define(name="mixed-workflow") +class MixedWorkflow: + @workflows.workflow.entrypoint + async def execute(self, email: str) -> dict: + # Fast lookup runs locally + with run_activities_locally(): + domain = await quick_lookup(email) + + # Slow operation runs as regular activity + verified = await external_api_call(email) + + return {"domain": domain, "verified": verified} +``` + +### Performance Impact + +| Execution Mode | Typical Latency | Best For | +| ---------------- | --------------- | --------------------------- | +| Local Activity | < 10ms | Quick computations, lookups | +| Regular Activity | 50-200ms | API calls, complex logic | + +### Limitations + +### Limitations + +**Bypasses Temporal Scheduling:** + +- No task queue isolation - activities share resources with workflows +- Worker process blocking - slow local activities block the workflow worker +- No independent scaling - cannot scale local activities separately +- Limited failure isolation - worker crashes affect both workflows and local activities + +**⚠️ Warning:** Local activities can cause worker crashes if they fail unexpectedly. Unlike regular activities, local activity failures can bring down the entire worker process, potentially causing data loss and requiring manual intervention to restart the workflow. +**Activity settings (partial support):** +Timeouts, retry policies, and dependency injection work the same for local activities. However, rate limiting is NOT supported for local activities. + +**Note:** The `sticky_to_worker`, `rate_limit`, and `heartbeat_timeout` parameters do not apply to local activities since they already run in the workflow worker process. + +```python +@workflows.activity( + start_to_close_timeout=timedelta(seconds=5), + retry_policy_max_attempts=3 +) +async def timed_local_activity(data: str) -> str: + # Timeouts and retry policies apply even when running locally + # Rate limiting is NOT supported for local activities + return data.upper() +``` + +## Sticky Worker Sessions + +Sticky worker sessions route multiple activities to the same worker instance, enabling resource reuse and stateful operations across activity calls. + +### When to Use + +**Good for:** + +- Sharing expensive resources (ML models, database connections, cached data) +- Stateful data processing across multiple activity calls +- Reducing initialization overhead for related operations + +**Avoid for:** + +- Activities that must be highly available (session breaks if worker crashes) +- Long-running workflows (ties up specific worker resources) +- When worker-level state isn't needed + +### Basic Usage + +```python +from mistralai.workflows import run_sticky_worker_session + +## Worker-level state +_loaded_model = None + +@workflows.activity(sticky_to_worker=True) +async def load_model() -> None: + global _loaded_model + if _loaded_model is None: + _loaded_model = load_expensive_model() + +@workflows.activity(sticky_to_worker=True) +async def predict(data: dict) -> dict: + return _loaded_model.predict(data) + +@workflows.workflow.define(name="ml-inference") +class MLInferenceWorkflow: + @workflows.workflow.entrypoint + async def execute(self, batch: list[dict]) -> list[dict]: + # All activities run on same worker + async with run_sticky_worker_session(): + await load_model() # Load once + results = [await predict(item) for item in batch] + return results +``` + +### Session Reuse + +Capture a session explicitly to reuse across multiple scopes: + +```python +@workflows.workflow.define(name="multi-batch") +class MultiBatchWorkflow: + @workflows.workflow.entrypoint + async def execute(self, batches: list[list[dict]]) -> list[list[dict]]: + # Capture worker once + session = await get_sticky_worker_session() + + all_results = [] + for batch in batches: + async with run_sticky_worker_session(session): + results = [await process_item(item) for item in batch] + all_results.append(results) + return all_results +``` + +### Limitations + +**Session breaks on worker failure:** + +- If the worker crashes or scales down, the session ends +- Subsequent activities route to a different worker +- Design activities to handle cold starts gracefully + +**In-memory state only:** + +- Worker-level state is lost on worker restart or redeployment +- Use databases or external storage for persistent state +- Not a replacement for distributed state management + +**Worker resource contention:** + +- Long-running sessions tie up specific worker capacity +- Can create hot spots if many workflows target the same worker +- Monitor worker utilization to avoid resource starvation + +**Note:** The `sticky_to_worker` parameter does not apply to local activities since they already run in the workflow worker process. + +### Performance Comparison + +| Feature | Regular Activity | Sticky Session | Local Activity | +| ---------------- | ---------------- | -------------- | -------------- | +| Routing Overhead | Standard | Standard | None | +| Worker Isolation | Yes | Yes | No | +| Resource Sharing | No | Yes | N/A | +| Best For | Independent ops | Resource reuse | Fast lookups | + +## Next Steps + +- [Workflows Guide](workflows) - Learn how to orchestrate activities +- [Observability Guide](observability) - Monitor and trace your activities diff --git a/.claude/skills/workflows/references/guides/assist-workflows.mdx b/.claude/skills/workflows/references/guides/assist-workflows.mdx new file mode 100644 index 000000000..f8fd95e91 --- /dev/null +++ b/.claude/skills/workflows/references/guides/assist-workflows.mdx @@ -0,0 +1,1045 @@ +--- +id: conversational-workflows +title: Conversational Workflows +sidebar_position: 9 +--- + +# Conversational Workflows + +Conversational workflows are meant to be integrated in conversations interface, allowing a user to trigger a workflow, +interact with it by providing inputs during its execution, and follow its progress. + +## Getting started + +To use Conversational workflow features, install the Mistral plugin: + +```bash +uv add 'mistralai-workflows[mistralai]' +``` + +The simplest conversational workflow sends an assistant message to the user, then waits for their response. +Extend `InteractiveWorkflow` and use `send_assistant_message()` followed by `wait_for_input()` with `ChatInput`: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="greeting-workflow", + workflow_display_name="Greeting", + workflow_description="A simple conversational workflow", +) +class GreetingWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Send a message to the user + await workflows_mistralai.send_assistant_message( + "Hello! I'm here to help you get started. What's your name?" + ) + + # Wait for the user's response + user_input = await self.wait_for_input( + workflows_mistralai.ChatInput() + ) + + name = user_input.message[0].text if user_input.message else "friend" + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"Nice to meet you, {name}!")] + ) +``` + +`send_assistant_message()` displays a message to the user in the chat interface. It also accepts an optional `canvas` keyword argument to include a `CanvasResource` alongside the text (see [Canvas Editing](#canvas-editing-human-in-the-loop)). `ChatInput()` pauses the workflow and waits for the user to respond. You can optionally pass a `prompt` to `ChatInput()` to provide additional context (in placeholder) and `suggestions` to offer pre-filled options that users can select directly. + +### Timeout + +`wait_for_input()` accepts an optional `timeout` parameter. If the user does not respond within the specified duration, an `asyncio.TimeoutError` is raised. The timeout can be a `timedelta` or a number of seconds (`float`). By default, the workflow waits indefinitely. + +```python +from datetime import timedelta + +## Wait for user response, but time out after 5 minutes +user_input = await self.wait_for_input( + workflows_mistralai.ChatInput(), + timeout=timedelta(minutes=5), +) +``` + +:::tip +Wrap the call in a `try`/`except asyncio.TimeoutError` block to handle the timeout gracefully instead of failing the workflow. +::: + +## Structured Form Inputs + +For workflows that need structured form input with typed fields, validation, and custom UI rendering, use `FormInput` instead of `ChatInput`: + +```python +from datetime import date, datetime + +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai +from mistralai.workflows.conversational import ( + FormInput, + TextField, + NumberField, + DateField, + DateTimeField, + SingleChoice, +) + +class ExpenseForm(FormInput): + """Structured form for expense submission.""" + + description: str = TextField(description="Expense description") + amount: float = NumberField( + description="Amount in USD", + minimum=0, + maximum=10000, + ) + category: str = SingleChoice( + options=[ + ("travel", "Travel"), + ("equipment", "Equipment"), + ("software", "Software"), + ], + description="Expense category", + ) + expense_date: date = DateField(description="Date of expense") + due_date: datetime = DateTimeField(description="Reimbursement due date") + receipt_id: str = TextField( + description="Receipt ID", + pattern=r"^RCP-\d{6}$", + ) + +@workflows.workflow.define( + name="expense-submission-workflow", + workflow_display_name="Expense Submission", + workflow_description="Submit an expense with structured form", +) +class ExpenseSubmissionWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + expense = await self.wait_for_input( + ExpenseForm, + label="Submit Expense", + ) + + result = f"""Expense submitted: +- Description: {expense.description} +- Amount: ${expense.amount:.2f} +- Category: {expense.category} +- Date: {expense.expense_date.isoformat()} +- Due date: {expense.due_date.isoformat()} +- Receipt: {expense.receipt_id}""" + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=result)] + ) +``` + +### Field Types + +| Field Type | Description | Properties | +| --------------- | ---------------- | ----------------------------------------------------------------------------- | +| `TextField` | Text input | `description`, `pattern` (optional regex) | +| `NumberField` | Numeric input | `description`, `minimum`, `maximum`, `exclusive_minimum`, `exclusive_maximum` | +| `DateTimeField` | Date/time picker | `description` | +| `DateField` | Date picker | `description` | +| `SingleChoice` | Dropdown/select | `options` (list of tuples or strings), `description` | +| `MultiChoice` | Multi-select | `options` (list of tuples or strings), `description` | +| `FileField` | File upload | `description`, `multiple` (default `False`) | + +### TextField + +```python +name: str = TextField(description="Your name") +email: str = TextField( + description="Email address", + pattern=r"^[\w.-]+@[\w.-]+\.\w+$", # Optional regex validation +) +``` + +### NumberField + +```python +amount: float = NumberField( + description="Amount", + minimum=0, # Inclusive minimum + maximum=10000, # Inclusive maximum +) +price: float = NumberField( + description="Price", + exclusive_minimum=0, # Must be greater than 0 + exclusive_maximum=100, # Must be less than 100 +) +``` + +### DateTimeField + +```python +from datetime import datetime + +scheduled_at: datetime = DateTimeField(description="Schedule date and time") +``` + +### DateField + +```python +from datetime import date + +scheduled_at: date = DateField(description="Schedule date") +``` + +### SingleChoice + +```python +## With labels (value, display_label) +priority: str = SingleChoice( + options=[ + ("low", "Low Priority"), + ("medium", "Medium Priority"), + ("high", "High Priority"), + ], + description="Select priority", +) + +## Simple string options (value = label) +status: str = SingleChoice( + options=["pending", "approved", "rejected"], + description="Status", +) +``` + +### MultiChoice + +```python +## With labels (value, display_label) +tags: list[str] = MultiChoice( + options=[ + ("frontend", "Frontend"), + ("backend", "Backend"), + ("infra", "Infrastructure"), + ], + description="Select applicable tags", +) + +## Simple string options (value = label) +colors: list[str] = MultiChoice( + options=["red", "green", "blue"], + description="Pick colors", +) +``` + +### FileField + +```python +from mistralai.workflows.conversational import FileField + +## Single file upload +document: str = FileField(description="Upload a document") + +## Multiple file uploads +attachments: list[str] = FileField(description="Upload files", multiple=True) +``` + +The workflow receives URLs (strings) pointing to files uploaded by the user. To process a file, download it from the provided URL within your workflow. These URLs may expire, so if your workflow needs long-term access to the files, it is responsible for storing them elsewhere. + +## Confirmation Inputs + +For workflows that need a simple single-choice confirmation with direct submit, use `ConfirmationInput` or `AcceptDeclineConfirmation`. These helpers create a single-field form where selecting an option immediately submits the form. + +### ConfirmationInput + +`ConfirmationInput` provides a list of options that should be rendered as buttons. Selection of an option should immediately submit the form: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="type-selection-workflow", + workflow_display_name="Type Selection", + workflow_description="Select your favorite type", +) +class TypeSelectionWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + await workflows_mistralai.send_assistant_message("Let's find out your type preference!") + + selection = await self.wait_for_input( + workflows_mistralai.ConfirmationInput( + options=[ + ("fire", "Fire"), + ("water", "Water"), + ("grass", "Grass"), + ("electric", "Electric"), + ], + description="What is your favorite type?", + ) + ) + + selected_type = selection.choice # Returns the value, e.g., "fire" + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"You selected {selected_type}!")] + ) +``` + +| Property | Type | Description | +| ------------- | -------------------------------------- | ------------------------------------------------------------ | +| `options` | `list[tuple[str, str]]` or `list[str]` | List of options as `(value, label)` tuples or simple strings | +| `description` | `str` | Description shown above the options | + +The returned object has a `choice` property containing the selected option value. + +### AcceptDeclineConfirmation + +`AcceptDeclineConfirmation` is a specialized confirmation with two options: accept and decline. Clients can render this as a standard validation UI with keyboard shortcuts for quick responses: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="approval-workflow", + workflow_display_name="Approval", + workflow_description="Confirm an action", +) +class ApprovalWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + confirmation = await self.wait_for_input( + workflows_mistralai.AcceptDeclineConfirmation( + description="Do you want to proceed with this action?", + accept_label="Yes, proceed", + decline_label="Cancel", + ) + ) + + if workflows_mistralai.is_accepted(confirmation): + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text="Action confirmed!")] + ) + else: + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text="Action cancelled.")] + ) +``` + +| Property | Type | Description | +| --------------- | ----- | ----------------------------------- | +| `description` | `str` | Description shown above the buttons | +| `accept_label` | `str` | Label for the accept button | +| `decline_label` | `str` | Label for the decline button | + +Use the `is_accepted()` helper function to check whether the user accepted or declined: + +```python +if workflows_mistralai.is_accepted(confirmation): + # User accepted + pass +else: + # User declined + pass +``` + +## Todo List Progress Tracking + +Display a checklist of steps with real-time status updates using `TodoList`: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="expense-processing-workflow", + workflow_display_name="Expense Processing", + workflow_description="Process expense with step-by-step progress", +) +class ExpenseProcessingWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self, expense_id: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Define the steps + validate_item = workflows_mistralai.TodoListItem( + title="Validate expense", + description="Check expense details and receipts" + ) + approve_item = workflows_mistralai.TodoListItem( + title="Get approval", + description="Route to manager for approval" + ) + process_item = workflows_mistralai.TodoListItem( + title="Process payment", + description="Submit for reimbursement" + ) + + async with workflows_mistralai.TodoList( + items=[validate_item, approve_item, process_item] + ) as todo_list: + # Step 1: Validate (using context manager for automatic status) + async with validate_item: + pass # ... validation logic ... + + # Step 2: Approve (using context manager for automatic status) + async with approve_item: + pass # ... approval logic ... + + # Step 3: Process (using context manager for automatic status) + async with process_item: + pass # ... processing logic ... + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"Expense {expense_id} processed successfully")] + ) +``` + +### Updating Item Status + +There are two ways to update the status of a `TodoListItem`: + +**Context Manager:** + +```python +async with item: + # Status automatically set to "in_progress" on enter + # ... do work ... + # Status automatically set to "done" on successful exit +``` + +**Manual Control (for fine-grained status updates):** + +```python +await item.set_status("in_progress") +## ... do work ... +await item.set_status("done") +``` + +Use manual control when you need to update status at specific points, handle conditional flows, or implement custom exception handling. + +### TodoListItem Properties + +| Property | Type | Description | +| ------------- | ----------------------------------- | ------------------------------------- | +| `id` | `str` | Auto-generated UUID | +| `title` | `str` | Display title for the step | +| `description` | `str` | Detailed description | +| `status` | `"todo" \| "in_progress" \| "done"` | Current status (defaults to `"todo"`) | + +## Streaming Agent Responses + +When using agents with `RemoteSession(stream=True)`, responses are automatically streamed to the UI as custom events. No additional code is needed: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="expense-analysis-workflow", + workflow_display_name="Expense Analysis", + workflow_description="AI-powered expense analysis", +) +class ExpenseAnalysisWorkflow: + @workflows.workflow.entrypoint + async def run(self, expense_data: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Create a streaming session - responses will automatically + # stream to the UI as they are generated + session = workflows_mistralai.RemoteSession(stream=True) + + analyst_agent = workflows_mistralai.Agent( + model="mistral-medium-2508", + name="expense-analyst", + description="Analyzes expense reports for policy compliance", + instructions="""You are an expense report analyst. Review the expense data +and provide insights on: +1. Policy compliance +2. Unusual patterns +3. Optimization suggestions + +Be concise and professional.""", + ) + + # The agent's response streams automatically custom events with JSON patches to update the UI. + await workflows_mistralai.Runner.run( + agent=analyst_agent, + inputs=f"Analyze this expense report:\n\n{expense_data}", + session=session, + ) + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text="Analysis complete.")] + ) +``` + +:::tip +When `stream=True`, the agent's text output is streamed token-by-token to the UI. This provides a responsive experience for longer responses. +::: + +## Rich Outputs (Canvas) + +Return rich content like markdown, code, or diagrams using `ResourceOutput`: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="expense-report-workflow", + workflow_display_name="Expense Report", + workflow_description="Generate expense report with charts", +) +class ExpenseReportWorkflow: + @workflows.workflow.entrypoint + async def run(self, department: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Generate a mermaid chart + chart_content = """ +pie title Expenses by Category + "Travel" : 45 + "Equipment" : 25 + "Software" : 20 + "Other" : 10 +""" + + canvas = workflows_mistralai.CanvasPayload( + type="mermaid", + title="Expense Breakdown", + content=chart_content, + ) + + resource = workflows_mistralai.CanvasResource( + canvas=canvas, + ) + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[ + workflows_mistralai.TextOutput(text=f"Expense report for {department}:"), + workflows_mistralai.ResourceOutput(resource=resource), + ] + ) +``` + +### Canvas Types + +| Type | Description | +| --------------- | ----------------------------- | +| `text/markdown` | Markdown content | +| `text/html` | HTML content | +| `image/svg+xml` | SVG images | +| `slides` | Presentation slides | +| `react` | React components | +| `code` | Code with syntax highlighting | +| `mermaid` | Mermaid diagrams | + +## Canvas Editing (Human-in-the-Loop) + +You can send a canvas mid-workflow using `send_assistant_message()` and then let the user edit it. +The `canvas_uri` passed to `CanvasInput` must match the `uri` of a `CanvasResource` output from a previous step. + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai +from mistralai.workflows.conversational import CanvasInput + +@workflows.workflow.define( + name="report-review-workflow", + workflow_display_name="Report Review", + workflow_description="Generate a report and let the user edit it", +) +class ReportReviewWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Send a canvas to the user as an assistant message + canvas_resource = workflows_mistralai.CanvasResource( + canvas=workflows_mistralai.CanvasPayload( + type="text/markdown", + title="Weekly Report", + content="# Weekly Report\n\n## Summary\n\nTODO: fill in", + ), + ) + await workflows_mistralai.send_assistant_message( + "Here is your report draft. You can review and edit it below.", + canvas=canvas_resource, + ) + + # Wait for the user to edit the canvas + edited = await self.wait_for_input( + CanvasInput(canvas_uri=canvas_resource.uri, prompt="Any feedback?"), + label="Review & Edit Report", + ) + + # Use the edited content + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[ + workflows_mistralai.TextOutput(text="Report finalized!"), + workflows_mistralai.ResourceOutput( + resource=workflows_mistralai.CanvasResource( + canvas=workflows_mistralai.CanvasPayload( + type="text/markdown", + title=edited.canvas.title, + content=edited.canvas.content, + ), + ) + ), + ] + ) +``` + +`CanvasInput` returns a model with: + +- `canvas.title` — the title of the edited canvas +- `canvas.content` — the edited content +- `chatInput` — optional chat message (only present when `prompt` is provided and the user sends a message) + +## Rich UI Components + +Workflows can render rich, interactive UI components in the chat interface using the design system component library. Components are defined as Python objects and sent as `UIComponentResource` resources. + +### Basic Usage + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai +from mistralai.workflows.conversational_ui_components import ( + Badge, + Card, + Column, + Markdown, + Row, +) + +@workflows.workflow.define( + name="report-workflow", + workflow_display_name="Report", + workflow_description="Generate a rich UI report", +) +class ReportWorkflow: + @workflows.workflow.entrypoint + async def run(self) -> workflows_mistralai.ChatAssistantWorkflowOutput: + report = Card( + title="Summary", + children=[ + Row( + children=[ + Markdown(content="**Score:** 0.82"), + Badge(variant="success", children="Pass"), + ], + ), + ], + ) + + await workflows_mistralai.send_assistant_message( + [ + workflows_mistralai.TextOutput(text="Here is your report:"), + workflows_mistralai.ResourceOutput( + resource=workflows_mistralai.UIComponentResource(component=report), + ), + ] + ) + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[ + workflows_mistralai.ResourceOutput( + resource=workflows_mistralai.UIComponentResource(component=report), + ), + ], + ) +``` + +### Available Components + +All components are imported from `mistralai.workflows.conversational_ui_components`. + +| Component | Description | Key Props | +| ------------ | --------------------------------------------- | --------------------------------------------------------------------- | +| `Alert` | Important messages with severity levels | `title`, `variant` (info/warning/error/success), `children` | +| `Avatar` | User avatar image | `src`, `alt`, `text`, `size` | +| `Badge` | Small label for status indicators | `variant` (default/primary/success/warning/error), `size`, `children` | +| `ButtonLink` | Link styled as a button | `href`, `variant`, `size`, `external`, `children` | +| `Card` | Container with optional title and description | `title`, `description`, `padding`, `children` | +| `Chart` | Line or bar chart | `variant` (line/bar), `data`, `xAxis`, `yAxis`, `title` | +| `Column` | Vertical layout container | `alignment`, `distribution`, `gap`, `children` | +| `Image` | Image display | `src`, `alt`, `size` | +| `Markdown` | Markdown-formatted text | `content` | +| `PieChart` | Pie chart with labeled segments | `data`, `title` | +| `Row` | Horizontal layout container | `alignment`, `distribution`, `gap`, `wrap`, `children` | +| `Tooltip` | Additional information on hover | `trigger`, `children` | + +### Nesting Components + +Components that accept `children` can contain other components, allowing you to build complex layouts: + +```python +from mistralai.workflows.conversational_ui_components import ( + Card, + Chart, + Column, + Row, +) + +layout = Row( + children=[ + Card( + title="Revenue", + children=[ + Chart( + variant="line", + xAxis="month", + yAxis=["actual", "target"], + data=[ + {"month": "Jan", "actual": 100, "target": 120}, + {"month": "Feb", "actual": 140, "target": 130}, + {"month": "Mar", "actual": 160, "target": 140}, + ], + ), + ], + ), + Card( + title="Distribution", + children=[ + Chart( + variant="bar", + xAxis="category", + yAxis="count", + data=[ + {"category": "A", "count": 42}, + {"category": "B", "count": 28}, + {"category": "C", "count": 15}, + ], + ), + ], + ), + ], +) +``` + +## Tool UI States + +Tool UI States provide optional visual representations of tool execution in the chat interface. When attached to `ChatAssistantWorkingTask`, they enable specialized UI feedback for different types of tool operations, allowing workflows to display the status and results of tool calls in a structured, user-friendly way. + +### Tool UI State Types + +There are three types of tool UI states: + +#### File Tool UI State + +Represents file operations such as creating, replacing, or deleting files: + +```python +from mistralai.workflows.conversational import ( + FileToolUIState, + CreateFileOperation, + ReplaceFileOperation, + DeleteFileOperation, +) + +## Create a file +create_state = FileToolUIState( + toolCallId="tc-1", + operations=[ + CreateFileOperation( + uri="file:///workspace/new.py", + content="print('Hello World')" + ) + ], +) + +## Replace file content +replace_state = FileToolUIState( + toolCallId="tc-2", + operations=[ + ReplaceFileOperation( + uri="file:///workspace/main.py", + fileContentBefore="old content", + blocks=[SearchReplaceBlock(search="old", replace="new")], + ) + ], +) + +## Delete a file +delete_state = FileToolUIState( + toolCallId="tc-3", + operations=[ + DeleteFileOperation(uri="file:///workspace/old.py") + ], +) +``` + +#### Generic Tool UI State + +Represents generic tool execution with various status states: + +```python +from mistralai.workflows.conversational import ( + GenericToolUIState, + ToolResultRunning, + ToolResultSuccess, + ToolResultFailed, +) + +## Tool is running +running_state = GenericToolUIState( + toolCallId="tc-1", + name="bash", + arguments={"command": "ls -la"}, + result=ToolResultRunning(), +) + +## Tool completed successfully +success_state = GenericToolUIState( + toolCallId="tc-2", + name="grep", + arguments={"pattern": "TODO"}, + result=ToolResultSuccess(value={"matches": ["line1", "line2"]}), +) + +## Tool failed +failed_state = GenericToolUIState( + toolCallId="tc-3", + name="bash", + arguments={"command": "false"}, + result=ToolResultFailed(error="exit code 1"), +) +``` + +#### Command Tool UI State + +Represents command execution with running/success/failed states: + +```python +from mistralai.workflows.conversational import ( + CommandToolUIState, + CommandResultRunning, + CommandResultSuccess, + CommandResultFailed, +) + +## Command is running +running_state = CommandToolUIState( + toolCallId="tc-1", + command="npm install", + result=CommandResultRunning(), +) + +## Command completed successfully +success_state = CommandToolUIState( + toolCallId="tc-2", + command="pytest", + result=CommandResultSuccess(output="All tests passed"), +) + +## Command failed +failed_state = CommandToolUIState( + toolCallId="tc-3", + command="invalid-command", + result=CommandResultFailed(error="Command not found"), +) +``` + +### Using Tool UI States in Working Tasks + +Tool UI States can be attached to `ChatAssistantWorkingTask` to provide visual feedback during tool execution: + +```python +from mistralai.workflows.conversational import ChatAssistantWorkingTask + +## Show a working task with file operations +task = ChatAssistantWorkingTask( + title="Creating file", + content="Generating new.py", + toolUIState=FileToolUIState( + toolCallId="tc-1", + operations=[CreateFileOperation(uri="file:///workspace/new.py", content="print('hi')")], + ), +) + +## Show a working task with command execution +command_task = ChatAssistantWorkingTask( + title="Running tests", + content="Executing pytest", + toolUIState=CommandToolUIState( + toolCallId="tc-2", + command="pytest", + result=CommandResultRunning(), + ), +) +``` + +## Publish in Le Chat + +To publish a conversational workflow as an assistant in Le Chat (Mistral's chat interface), your workflow must return a `ChatAssistantWorkflowOutput`. +The output will not be displayed in Le Chat, but we enforce a common interface for inter-operability. + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="expense-summary-workflow", + workflow_display_name="Expense Summary", + workflow_description="Generates a summary of expenses", +) +class ExpenseSummaryWorkflow: + @workflows.workflow.entrypoint + async def run(self, department: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + summary = f"Expense summary for {department}: Total $12,500" + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=summary)] + ) +``` + +### Tagging Input Variants + +When a workflow accepts a union of input types, clients may need a way to know which variant to use. The `@input_tag` decorator adds an `x-input-tag` field to a model's JSON schema so clients can identify and select the right one. + +```python +from pydantic import BaseModel + +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai +from mistralai.workflows.plugins.mistralai import input_tag + +class FullParams(BaseModel): + config: str + options: dict[str, str] + +@input_tag("simplified") +class SimpleParams(BaseModel): + prompt: str + +@workflows.workflow.define( + name="multi-client-workflow", + workflow_display_name="Multi-Client", +) +class MultiClientWorkflow: + @workflows.workflow.entrypoint + async def run(self, params: FullParams | SimpleParams) -> workflows_mistralai.ChatAssistantWorkflowOutput: + if isinstance(params, SimpleParams): + resolved = params.prompt + else: + resolved = params.config + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"Received: {resolved}")] + ) +``` + +`SimpleParams.model_json_schema()` now contains `"x-input-tag": "simplified"`. Clients inspect the union members in the workflow's `input_schema` and select the variant whose tag they recognise. + +### Error Handling + +To signal that a workflow has failed, set `isError=True` on the output. +The text content is used as the error message displayed to the user, and the workflow is marked as failed in the conversation: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="expense-validation-workflow", + workflow_display_name="Expense Validation", + workflow_description="Validates expense data", +) +class ExpenseValidationWorkflow: + @workflows.workflow.entrypoint + async def run(self, expense_id: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + expense = lookup_expense(expense_id) + if expense is None: + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"Expense {expense_id} not found.")], + isError=True, + ) + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=f"Expense {expense_id} is valid.")] + ) +``` + +### Structured Content + +`ChatAssistantWorkflowOutput` accepts an optional `structuredContent` field (`dict[str, Any]`) +for attaching arbitrary structured data to the workflow output. + +```python +return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text="Done.")], + structuredContent={"tool": "web_search", "results": [{"url": "https://example.com"}]}, +) +``` + +## Complete Example + +Here's a full expense approval workflow combining todo list, user input, and streaming agent analysis: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.workflow.define( + name="full-expense-workflow", + workflow_display_name="Full Expense Processing", + workflow_description="Complete expense workflow with AI analysis and approval", +) +class FullExpenseWorkflow(workflows.InteractiveWorkflow): + @workflows.workflow.entrypoint + async def run(self, expense_data: str) -> workflows_mistralai.ChatAssistantWorkflowOutput: + # Define workflow steps + analyze_item = workflows_mistralai.TodoListItem( + title="AI Analysis", + description="Analyze expense for compliance" + ) + review_item = workflows_mistralai.TodoListItem( + title="Manager Review", + description="Get manager approval" + ) + process_item = workflows_mistralai.TodoListItem( + title="Process", + description="Complete processing" + ) + + async with workflows_mistralai.TodoList( + items=[analyze_item, review_item, process_item] + ) as todo_list: + # Step 1: AI Analysis with streaming (using context manager) + async with analyze_item: + session = workflows_mistralai.RemoteSession(stream=True) + analyst = workflows_mistralai.Agent( + model="mistral-medium-2508", + name="expense-analyst", + description="Expense policy analyst", + instructions="Analyze the expense for policy compliance. Be brief.", + ) + + await workflows_mistralai.Runner.run( + agent=analyst, + inputs=expense_data, + session=session, + ) + + # Step 2: Manager Review (using context manager) + async with review_item: + decision = await self.wait_for_input( + workflows_mistralai.ChatInput( + "Do you approve or reject this expense? Please explain.", + suggestions=[ + [workflows_mistralai.TextChunk(text="Yes, approve this expense")], + [workflows_mistralai.TextChunk(text="Reject")], + ], + ) + ) + + # Step 3: Process (using manual status control for conditional logic) + await process_item.set_status("in_progress") + decision_text = decision.message[0].text if decision.message else "" + if "approve" in decision_text.lower(): + result = f"Expense approved. {decision_text}" + else: + result = f"Expense rejected. {decision_text}" + await process_item.set_status("done") + + return workflows_mistralai.ChatAssistantWorkflowOutput( + content=[workflows_mistralai.TextOutput(text=result)] + ) + +if __name__ == "__main__": + import asyncio + asyncio.run(workflows.run_worker([FullExpenseWorkflow])) +``` diff --git a/.claude/skills/workflows/references/guides/concurrency.mdx b/.claude/skills/workflows/references/guides/concurrency.mdx new file mode 100644 index 000000000..af0cc1bad --- /dev/null +++ b/.claude/skills/workflows/references/guides/concurrency.mdx @@ -0,0 +1,487 @@ +--- +id: concurrency +title: Concurrency Patterns +sidebar_position: 9 +--- + +# Concurrency Patterns: Scale Your Workflows + +_Process thousands of items efficiently with Mistral Workflows' parallel execution patterns_ + +## Overview + +Mistral Workflows provides a powerful concurrency framework that enables you to execute activities in parallel across three distinct patterns: + +- **List Executor**: Process a known collection of items +- **Chain Executor**: Process items sequentially from a stream/queue (token-based pagination) +- **Offset Pagination Executor**: Process items by fetching pages/chunks by index + +All patterns are built on Temporal's workflow continuation patterns, providing automatic fault tolerance, progress tracking, and scalability for large datasets. + +### Key Benefits + +- **Massive Parallelism**: Execute thousands of activities concurrently +- **Automatic Continue-As-New**: Handle large datasets without hitting Temporal's execution history limits +- **Type Safety**: Comprehensive type validation for all inputs and outputs +- **Fault Tolerance**: Built-in error handling and retry mechanisms +- **Progress Tracking**: Monitor execution progress through built-in observability features + +## List Executor Pattern + +### Use Case + +Process a known collection of items where you have all items upfront. + +### When to Use + +- Database query results +- File contents or uploaded documents +- API responses that return all items at once +- Batch processing of users, files, or records + +### Code Pattern + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def process_item(item_id: int, value: str) -> dict: + # Process individual item + return {"processed_value": f"processed_{value}"} + +## ... (inside a workflow) +## Execute in parallel +items = [{"item_id": i, "value": f"item_{i}"} for i in range(1000)] +results = await workflows.execute_activities_in_parallel( + activity=process_item, + items=items, + max_concurrent_scheduled_tasks=100 # Optional: limit concurrency +) +## ... +``` + +### Configuration Options + +| Parameter | Description | Default | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `max_concurrent_scheduled_tasks` | Maximum number of concurrent activity executions that can be scheduled simultaneously. This limits how many activities are waiting to be executed at once. | 100 | +| `extra_params` | Additional parameters to pass to the activity | None | + +**Note**: The List Executor does not use `max_concurrent_executions_per_worker` parameter. This parameter is only relevant for the Offset Pagination Executor. + +## Chain Executor Pattern + +### Use Case + +Process items sequentially from a stream/queue using token-based pagination. + +### When to Use + +- AWS S3 ListObjects (uses ContinuationToken) +- DynamoDB Scan/Query (uses LastEvaluatedKey) +- Azure Blob Storage (uses marker) +- Any API that uses continuation tokens instead of page numbers + +### Code Pattern + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def process_item(item_id: int, value: str) -> dict: + # Process individual item + return {"processed_value": f"processed_{value}"} + +@workflows.activity() +async def get_next_item(prev_item: dict | None) -> dict | None: + # Get next item from previous item + if prev_item is None: + # First item + return {"item_id": 0, "value": "item_0"} + + next_id = prev_item["item_id"] + 1 + if next_id >= 1000: # Stop condition + return None + + return {"item_id": next_id, "value": f"item_{next_id}"} + +## ... (inside a workflow) +## Execute chain +results = await workflows.execute_activities_in_parallel( + activity=process_item, + get_item_from_prev_item_activity=get_next_item +) +## ... +``` + +**Note**: The Chain Executor does not use `max_concurrent_scheduled_tasks` or `max_concurrent_executions_per_worker` parameters. + +### How It Works + +The Chain Executor separates **item discovery** (sequential) from **item processing** (parallel): + +1. **Item fetching is chained**: The executor calls `get_item_from_prev_item_activity` sequentially—first with `None` to get the first item, then with each result to get the next item, until the function returns `None` +2. **Processing is parallelized**: As soon as an item is fetched, it's immediately dispatched for processing via the `activity` function. Items don't wait for each other to finish processing + +This means fetching item N+1 depends on item N's result, but _processing_ item N+1 runs concurrently with processing items 1 through N. + +## Offset Pagination Executor Pattern + +### Use Case + +Process items by fetching pages/chunks using index-based pagination. + +### When to Use + +- Traditional REST APIs with page numbers +- SQL database chunking with OFFSET/LIMIT +- File processing by byte offset +- Any index-based data fetching + +### Code Pattern + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def process_item(item_id: int, value: str) -> dict: + # Process individual item + return {"processed_value": f"processed_{value}"} + +@workflows.activity() +async def get_item_by_index(params: workflows.GetItemFromIndexParams) -> dict: + # Get item by index + return { + "item_id": params.idx, + "value": f"item_{params.idx}", + "extra_data": params.extra_params + } + +## Execute with offset pagination +results = await workflows.execute_activities_in_parallel( + activity=process_item, + get_item_from_index_activity=get_item_by_index, + n_items=1000, # Total number of items + max_concurrent_executions_per_worker=50, # Optional: controls how many items are processed together + max_concurrent_scheduled_tasks=100, # Optional: limit concurrent activity executions + extra_params={"batch_id": "daily_processing"} # Optional: extra parameters +) +``` + +### Configuration Options + +| Parameter | Description | Default | +| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `n_items` | Total number of items to process | Required | +| `max_concurrent_scheduled_tasks` | Maximum number of concurrent activity executions that can be scheduled simultaneously. This limits how many activities are waiting to be executed at once. | 100 | +| `max_concurrent_executions_per_worker` | Controls how many items are processed together in a single activity execution. Only used by Offset Pagination Executor. | 100 | +| `extra_params` | Additional parameters to pass to the activity | None | + +**Note**: Items processed together are wrapped in a single activity. If any item fails, the entire group is retried together. + +## Offset Pagination Executor Best Practices + +### Understanding Parameter Interactions + +The Offset Pagination Executor uses two key parameters: + +1. **`max_concurrent_executions_per_worker`**: Controls how many items are processed together in a single activity execution. +2. **`max_concurrent_scheduled_tasks`**: Controls how many of these activity executions can run concurrently. + +**Example**: If you have 1000 items with `max_concurrent_executions_per_worker=10` and `max_concurrent_scheduled_tasks=5`, the system will: + +- Process items in groups of 10 +- Execute up to 5 groups concurrently +- Process all items efficiently while maintaining manageable group sizes + +### Retry Behavior Considerations + +**Important**: All items processed together are wrapped in a single activity. This means: + +- **Pros**: Efficient processing, reduced overhead +- **Cons**: If any single item in a group fails, the entire group is retried + +**Best Practices**: + +- Choose `max_concurrent_executions_per_worker` values that balance efficiency with retry granularity +- Smaller values: More fine-grained retries, but higher overhead +- Larger values: More efficient, but larger retry scope +- Consider item failure rates when choosing values + +### Performance Optimization Strategies + +1. **For I/O-bound tasks** (API calls, database queries): + + - Use larger groups (50-100 items) + - Higher `max_concurrent_scheduled_tasks` (100-200) + - Example: `max_concurrent_executions_per_worker=50, max_concurrent_scheduled_tasks=150` + +2. **For CPU-bound tasks**: + + - Use smaller groups (5-20 items) + - Lower `max_concurrent_scheduled_tasks` (20-50) + - Example: `max_concurrent_executions_per_worker=10, max_concurrent_scheduled_tasks=30` + +3. **For mixed workloads**: + - Medium groups (20-50 items) + - Balanced concurrency (50-100) + - Example: `max_concurrent_executions_per_worker=25, max_concurrent_scheduled_tasks=75` + +### Memory and Resource Considerations + +- **Group size impacts memory usage**: Larger groups consume more memory per activity +- **Concurrency impacts resource contention**: Higher concurrency may lead to resource exhaustion +- **Temporal limits**: Remember Temporal's 2MB input/output limit per activity + +## Advanced Features + +### Error Handling + +The concurrency framework automatically handles: + +- Activity failures with retry mechanisms +- Type validation errors +- Temporal workflow continuation for large datasets +- Progress tracking and state management + +### Performance Optimization + +#### Concurrency Limits + +Adjust concurrency based on your workload: + +```python +## For I/O-bound tasks (API calls, database queries) +results = await workflows.execute_activities_in_parallel( + activity=api_call_activity, + items=items, + max_concurrent_scheduled_tasks=200 # Higher concurrency for I/O-bound +) + +## For CPU-bound tasks +results = await workflows.execute_activities_in_parallel( + activity=cpu_intensive_activity, + items=items, + max_concurrent_scheduled_tasks=50 # Lower concurrency for CPU-bound +) +``` + +#### Batch Processing + +For very large datasets, the framework automatically handles continue-as-new: + +```python +## Process 100,000 items with List Executor - automatically continues as new workflow +results = await workflows.execute_activities_in_parallel( + activity=process_item, + items=large_item_list, # 100,000 items + max_concurrent_scheduled_tasks=100 +) + +## Process 100,000 items with Offset Pagination Executor +results = await workflows.execute_activities_in_parallel( + activity=process_item, + get_item_from_index_activity=get_item_by_index, + n_items=100000, + max_concurrent_executions_per_worker=100, # Process 100 items together per activity + max_concurrent_scheduled_tasks=50 # Execute 50 activities concurrently +) +``` + +## Real-World Examples + +### Document Processing Pipeline + +```python +@workflows.activity() +async def extract_text(document_id: str, content: str, metadata: dict) -> dict: + # Use OCR or text extraction + extracted_text = await ocr_service.extract(content) + return { + "document_id": document_id, + "extracted_text": extracted_text, + "analysis_results": {} + } + +## Process all documents in parallel +documents = await fetch_documents_from_storage() +results = await workflows.execute_activities_in_parallel( + activity=extract_text, + items=documents +) +``` + +### Batch Data Transformation + +```python +@workflows.activity() +async def enrich_user_data(user_id: str, name: str, email: str) -> dict: + # Fetch additional data from external services + profile_score = await analytics_service.calculate_score(user_id) + recommendations = await recommendation_service.get_recommendations(user_id) + + return { + "user_id": user_id, + "name": name, + "email": email, + "profile_score": profile_score, + "recommendations": recommendations + } + +## Enrich all users in parallel +users = await database.get_all_users() +enriched_users = await workflows.execute_activities_in_parallel( + activity=enrich_user_data, + items=users, + max_concurrent_scheduled_tasks=50 +) +``` + +### Multi-Service Orchestration + +```python +@workflows.activity() +async def process_order(order_id: str, customer_id: str, items: list[str]) -> dict: + # Call multiple services in parallel + payment_result = await payment_service.process(order_id) + inventory_result = await inventory_service.reserve(items) + shipping_result = await shipping_service.schedule(order_id) + + return { + "order_id": order_id, + "status": "processed", + "fulfillment_details": { + "payment": payment_result, + "inventory": inventory_result, + "shipping": shipping_result + } + } + +## Process all orders in parallel +orders = await fetch_pending_orders() +results = await workflows.execute_activities_in_parallel( + activity=process_order, + items=orders +) +``` + +## Performance Considerations + +### Memory Usage + +- Large datasets are processed efficiently to avoid memory issues +- Temporal's continue-as-new mechanism ensures workflow state remains manageable +- Each activity execution is isolated with its own memory footprint + +### Network I/O Optimization + +- Activities can be executed on workers close to data sources +- Use `sticky_to_worker=True` for activities that benefit from locality +- Configure appropriate concurrency limits based on network bandwidth + +### Error Recovery + +- Failed activities are automatically retried according to their retry policy +- The framework maintains progress state, so only failed items need reprocessing + +### Monitoring and Alerting + +- Implement custom event recording with `workflows.record_event()` +- Set up alerts for long-running or failed executions + +## Troubleshooting + +### Common Issues + +**Issue**: `ValueError: 'activity' must be an activity, please decorate it with @workflows.activity` + +**Solution**: Ensure your activity function is decorated with `@workflows.activity()` + +**Issue**: `ValueError: Must specify one execution pattern` + +**Solution**: Provide exactly one of: `items`, `get_item_from_prev_item_activity`, or `get_item_from_index_activity` + +**Issue**: Excessive retries with Offset Pagination Executor + +**Solution**: If you're experiencing excessive retries with the Offset Pagination Executor, consider: + +- Reducing `max_concurrent_executions_per_worker` to create smaller groups of items +- Investigating individual item failures that might be causing entire groups to retry +- Adding better error handling within your activity to prevent failures + +**Issue**: Memory issues with large groups + +**Solution**: If you encounter memory issues: + +- Reduce `max_concurrent_executions_per_worker` to process fewer items together +- Ensure individual items are not too large (remember Temporal's 2MB limit) +- Monitor memory usage and adjust group sizes accordingly + +### Debugging Tips + +1. **Check Activity Signatures**: Ensure all activities have proper type annotations +2. **Validate Concurrency Limits**: Start with lower concurrency and increase gradually +3. **Use Logging**: Add detailed logging in your activities for debugging +4. **Offset Pagination Debugging**: For Offset Pagination Executor issues: + - Start with small values (e.g., `max_concurrent_executions_per_worker=1`) + - Check for individual item failures that might affect entire groups + +## Next Steps + +- [Workflows Guide](workflows) - Learn how to orchestrate activities +- [Activities Guide](activities) - Understand activity implementation details +- [Observability Guide](observability) - Monitor and trace your concurrent executions +- [Limitations Guide](limitations) - Understand Temporal's execution constraints + +## API Reference + +### `execute_activities_in_parallel()` Function + +```python +async def execute_activities_in_parallel( + activity: Callable[[T], Awaitable[U]], + *, + # List Executor + items: List[T] | None = None, + max_concurrent_scheduled_tasks: int = DEFAULT_MAX_CONCURRENT_SCHEDULED_TASKS, + + # Chain Executor + get_item_from_prev_item_activity: Callable[[T | None], Awaitable[T | None]] | None = None, + + # Offset Pagination Executor + get_item_from_index_activity: Callable[[GetItemFromIndexParams], Awaitable[T]] | None = None, + n_items: int | None = None, + max_concurrent_executions_per_worker: int = DEFAULT_MAX_CONCURRENT_EXECUTIONS_PER_WORKER, + + # Common + extra_params: Dict[str, Any] | None = None, +) -> None | List[U] +``` + +**Parameters**: + +- `activity`: The activity function to execute on each item +- `items`: List of items to process (List Executor) +- `get_item_from_prev_item_activity`: Function to get next item from previous (Chain Executor) +- `get_item_from_index_activity`: Function to get item by index (Offset Pagination Executor) +- `n_items`: Total number of items (Offset Pagination Executor) +- `max_concurrent_scheduled_tasks`: Maximum number of concurrent activity executions that can be scheduled simultaneously. Applies to List Executor and Offset Pagination Executor only. +- `max_concurrent_executions_per_worker`: **Only for Offset Pagination Executor** - Controls how many items are processed together in a single activity execution. +- `extra_params`: Extra parameters to pass to activities + +**Parameter Usage by Executor**: + +| Executor | `max_concurrent_scheduled_tasks` | `max_concurrent_executions_per_worker` | +| -------------------------- | -------------------------------- | -------------------------------------- | +| List Executor | ✅ Yes | ❌ No | +| Chain Executor | ❌ No | ❌ No | +| Offset Pagination Executor | ✅ Yes | ✅ Yes | + +**Returns**: + +- `None` if activity returns None +- `List[U]` list of activity results + +**Raises**: + +- `ValueError`: If activity is not properly decorated or parameters are invalid diff --git a/.claude/skills/workflows/references/guides/dependency-injection.mdx b/.claude/skills/workflows/references/guides/dependency-injection.mdx new file mode 100644 index 000000000..8020ea7d5 --- /dev/null +++ b/.claude/skills/workflows/references/guides/dependency-injection.mdx @@ -0,0 +1,185 @@ +--- +id: dependency-injection +title: Dependency Injection +sidebar_position: 6 +--- + +# Dependency Injection in Workflows + +Dependency injection provides a clean way to access shared resources in your workflows and activities. It automatically handles the creation and management of resources like database connections, API clients, or configuration objects, so you don't have to manually create and pass these around in your code. + +## How It Works + +The system supports multiple ways to provide dependencies, allowing you to choose the approach that best fits your resource's requirements: + +1. **Synchronous functions** - Ideal for simple configuration values or in-memory objects +2. **Asynchronous functions** - Perfect for I/O-bound dependencies like database connections +3. **Context managers** - Best for resources that require cleanup operations +4. **Generators** - Useful for streaming data or complex resource management + +When you declare a dependency in your activity or workflow, the system automatically provides that resource when needed, handling all the lifecycle management for you. + +### Dependency Lifecycle and Resource Sharing + +**Important**: All dependencies defined with `Depends()` are initialized once when the worker starts up and then shared across all activity executions. This means: + +- **Single Instance**: The same instance is reused for every activity call +- **Resource Efficiency**: Reduces connection overhead and resource consumption +- **Connection Pooling**: Database connections, API clients, and other resources are maintained and reused + +This approach is particularly beneficial for: + +- **Database connections**: Avoids creating new connections for each query +- **API clients**: Maintains persistent connections and authentication +- **Configuration objects**: Loads configuration once and shares it across all activities + +## Defining Dependencies + +### Synchronous Function Provider + +For simple configuration values or objects that don't require async initialization: + +```python +def get_config() -> dict: + """Provides application configuration""" + return { + "timeout": 30, + "retries": 3, + "api_url": "https://api.example.com" + } +``` + +### Asynchronous Function Provider + +For dependencies that require async initialization, like database connections: + +```python +async def get_db_connection() -> DatabaseConnection: + """Creates and returns a database connection""" + conn = await DatabaseConnection.create("postgres://user:pass@localhost/db") + return conn +``` + +### Context Manager Provider + +For resources that need proper cleanup, like sessions that require logout: + +```python +from contextlib import contextmanager + +@contextmanager +def get_logged_in_session(): + """Provides a session with automatic login/logout""" + session = Session() + session.login() + try: + yield session + finally: + session.logout() +``` + +### Async Context Manager Provider + +For async resources that need cleanup, like database connections: + +```python +from contextlib import asynccontextmanager + +@asynccontextmanager +async def get_db_connection_with_cleanup(): + """Provides a database connection with proper cleanup""" + conn = await DatabaseConnection.create("postgres://...") + try: + yield conn + finally: + await conn.close() +``` + +### Generator Provider + +For streaming data or complex resource management: + +```python +def get_data_stream(): + """Provides a stream of data""" + with open("data.txt") as f: + for line in f: + yield line.strip() +``` + +### Async Generator Provider + +For async streaming data sources: + +```python +async def get_async_data_stream(): + """Provides an async stream of data""" + async with aiofiles.open("data.txt") as f: + async for line in f: + yield line.strip() +``` + +## Using Dependencies in Activities + +Activities can declare their dependencies using the `Depends()` marker. The system will automatically provide these dependencies when the activity executes: + +```python +import mistralai.workflows as workflows +from mistralai.workflows import Depends + +@workflows.activity() +async def create_user( + name: str, + db: DatabaseConnection = Depends(get_db_connection), + config: dict = Depends(get_config), + session: Session = Depends(get_logged_in_session) +): + """Creates a new user with all required dependencies""" + timeout = config["timeout"] + db.query( + "INSERT INTO users (name) VALUES ($1)", + name + ) + session.track_event("user_created", {"user_id": name}) + return {"status": "success", "timeout_used": timeout} +``` + +## Common Use Cases + +### Database Connections + +A more complete database connection example with proper session management: + +```python +async def get_db_connection(): + """Provides a database connection with proper cleanup""" + engine = await create_db_engine("postgres://...") + SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False) + + @asynccontextmanager + async def get_session() -> AsyncIterator[AsyncSession]: + async with SessionLocal() as session: + try: + yield session + finally: + await session.close() + + return get_session +``` + +### API Clients + +Example of a payment service client with initialization: + +```python +async def get_payment_client() -> PaymentServiceClient: + """Creates and initializes a payment service client""" + client = PaymentServiceClient(api_key="your_key") + await client.initialize() + return client +``` + +## Next Steps + +- [Workflow Guide](workflows) - Learn more about workflow fundamentals +- [Activity Guide](activities) - Understand how activities complement workflows diff --git a/.claude/skills/workflows/references/guides/durable-agents.mdx b/.claude/skills/workflows/references/guides/durable-agents.mdx new file mode 100644 index 000000000..f1ef472c4 --- /dev/null +++ b/.claude/skills/workflows/references/guides/durable-agents.mdx @@ -0,0 +1,403 @@ +--- +id: durable-agents +title: Durable Agents +sidebar_position: 8 +--- + +# Durable Agents: LLM Agents on Mistral Workflows + +Durable Agents allow you to run LLM agents within your Mistral workflows. + +## Installation + +To use Durable Agents, install the Mistral plugin: + +```bash +uv add 'mistralai-workflows[mistralai]' +``` + +## What is a Durable Agent? + +A Durable Agent is an LLM agent that executes within a workflow, benefiting from: + +- **Durability**: Agent state is preserved across failures and restarts +- **Tool Integration**: Use activities as agent tools +- **Multi-Agent Handoffs**: Agents can delegate tasks to specialized agents +- **MCP Support**: Connect to external tools via Model Context Protocol (stdio / SSE) + +## Architecture Overview + +```mermaid +flowchart TB + subgraph Runner["Runner"] + direction TB + R[Runner.run] + end + + subgraph Session["Session"] + S[RemoteSession / LocalSession] + end + + subgraph Coordinator["Coordinator Agent"] + CA[deal-coordinator] + end + + subgraph Specialists["Specialist Agents"] + RA[risk-agent] + CompA[compliance-agent] + end + + subgraph Activities["Activities"] + A1[parse_deal_document] + A2[calculate_risk_score] + A3[check_compliance] + end + + subgraph BuiltIn["Built-in Tools"] + T1[WebSearchTool] + end + + subgraph External["External"] + MCP[MCP Server] + end + + R -->|manages| S + S -->|runs| CA + CA -->|handoff| RA + CA -->|handoff| CompA + CA -->|uses| A1 + RA -->|uses| A2 + RA -->|connects| MCP + CompA -->|uses| A3 + CompA -->|uses| T1 +``` + +The Runner orchestrates agent execution through a Session. Agents can hand off tasks to specialized agents and each agent can use activities, built-in Mistral tools or MCP servers as tools. + +## Core Components + +### Agent + +The `Agent` class defines an LLM agent with its model, instructions, tools and handoffs: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="my-agent", + description="Agent that performs specific tasks", + instructions="Use tools to complete the user's request.", + tools=[my_activity], # Workflows activities as tools + handoffs=[other_agent], # Agents to delegate to +) +``` + +### Runner + +The `Runner` executes an agent with user inputs and manages the conversation loop: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +outputs = await workflows_mistralai.Runner.run( + agent=agent, + inputs="What is the interest rate for 2024?", + session=session, + max_turns=10, +) +``` + +### Sessions + +Sessions manage agent state and API communication. Two session types are available: + +| Session | Use Case | Backend | +| --------------- | -------------------------- | --------------------- | +| `RemoteSession` | Production (recommended) | Mistral Agents SDK | +| `LocalSession` | Experimental / On-premises | Direct completion API | + +## Basic Example + +Here's a simple agent workflow that uses an activity as a tool: + +```python +import mistralai +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +@workflows.activity() +async def get_interest_rate(year: int) -> dict: + """Get the interest rate for a given year. + + Args: + year: The year to get the interest rate for + """ + # Your implementation here + return {"interest_rate": 1.62} + +@workflows.workflow.define(name="finance_agent_workflow") +class FinanceAgentWorkflow: + @workflows.workflow.entrypoint + async def entrypoint(self, question: str) -> dict: + session = workflows_mistralai.RemoteSession() + + agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="finance-agent", + description="Agent for financial queries", + instructions="Use tools to answer financial questions.", + tools=[get_interest_rate], + ) + + outputs = await workflows_mistralai.Runner.run( + agent=agent, + inputs=question, + session=session, + ) + + answer = "\n".join([ + output.text for output in outputs + if isinstance(output, mistralai.TextChunk) + ]) + + return {"answer": answer} +``` + +## Multi-Agent Handoffs + +Agents can delegate tasks to specialized agents using handoffs. The system automatically manages the handoff conversation: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +## Create a specialized agent for interest rate queries +interest_rate_agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="ecb-interest-rate-agent", + description="Agent for European Central Bank interest rate research", + instructions="Use tools to get the interest rate for a given year.", + tools=[get_interest_rate], +) + +## Main agent that can hand off to the specialist +finance_agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="finance-agent", + description="Agent for financial queries", + handoffs=[interest_rate_agent], # Can delegate to interest_rate_agent +) + +outputs = await workflows_mistralai.Runner.run( + agent=finance_agent, + inputs="What was the ECB interest rate in 2023?", + session=workflows_mistralai.RemoteSession(), +) +``` + +When the finance agent receives a query about ECB interest rates, it can automatically hand off to the specialized `interest_rate_agent`. + +## MCP Integration + +Connect to external tool servers using the Model Context Protocol. Two transport types are supported: + +### Stdio MCP Server + +For local command-line MCP servers: + +```python +from mistralai.workflows.plugins.mistralai import MCPStdioConfig + +mcp_config = MCPStdioConfig( + command="npx", + args=["-y", "@modelcontextprotocol/server-everything"], + name="server-everything", +) + +agent = Agent( + model="mistral-medium-latest", + name="mcp-agent", + description="Agent with access to MCP tools", + mcp_clients=[mcp_config], +) +``` + +### SSE MCP Server + +For remote MCP servers over Server-Sent Events: + +```python +from mistralai.workflows.plugins.mistralai import MCPSSEConfig + +mcp_config = MCPSSEConfig( + url="https://your-mcp-server.com/sse", + timeout=60, + name="remote-tools", + headers={"Authorization": "Bearer your-token"}, # Optional +) + +agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="sse-mcp-agent", + description="Agent with access to remote MCP tools", + mcp_clients=[mcp_config], +) +``` + +## Built-in Tools + +Use Mistral's built-in tools alongside activities: + +```python +import mistralai +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="web-search-agent", + description="Agent with web search capability", + instructions="Use web search to answer user questions", + tools=[mistralai.WebSearchTool()], +) +``` + +Available built-in tools: + +- `mistralai.WebSearchTool()` - Web search capability +- `mistralai.CodeInterpreterTool()` - Code execution +- `mistralai.ImageGenerationTool()` - Image generation +- `mistralai.DocumentLibraryTool()` - Document analysis + +## Session Types + +### RemoteSession (Recommended) + +Uses the Mistral Agents SDK for production workloads: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +session = workflows_mistralai.RemoteSession() + +outputs = await workflows_mistralai.Runner.run( + agent=agent, + inputs="Your question here", + session=session, +) +``` + +Features: + +- Full Agents SDK integration +- Automatic agent creation and updates +- Managed conversation state +- Production-ready + +### LocalSession (Experimental) + +Runs agents locally using the completion endpoint: + +```python +import mistralai.workflows as workflows +import mistralai.workflows.plugins.mistralai as workflows_mistralai + +session = workflows_mistralai.LocalSession() + +outputs = await workflows_mistralai.Runner.run( + agent=agent, + inputs="Your question here", + session=session, +) +``` + +Use cases: + +- On-premises deployments that does not have access to Agents (Bora) +- Development and testing +- Full context control + +:::warning +`LocalSession` is experimental and may be removed in future versions. Use `RemoteSession` for production workloads. +::: + +## Complete Workflow Example + +A full example combining activities, handoffs and workflow orchestration: + +```python +import asyncio +import mistralai +import mistralai.workflows as workflows + +@workflows.activity() +async def calculate_risk_score(deal_type: str, amount: float) -> dict: + """Calculate financial risk score for a deal. + + Args: + deal_type: The type of deal being analyzed + amount: The monetary amount of the deal + """ + risk_score = min(100.0, amount / 10000.0) + risk_factors = [] + if amount > 100000: + risk_factors.append("High value transaction") + return {"risk_score": risk_score, "risk_factors": risk_factors} + +@workflows.workflow.define(name="deal_analysis_workflow") +class DealAnalysisWorkflow: + @workflows.workflow.entrypoint + async def entrypoint(self, deal_request: str) -> dict: + """Analyze a deal request. + + Args: + deal_request: The deal request to analyze + """ + session = workflows_mistralai.RemoteSession() + + # Risk assessment agent + risk_agent = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="risk-agent", + description="Analyzes financial risk of deals", + instructions="Use the risk calculation tool to assess deal risk.", + tools=[calculate_risk_score], + ) + + # Main coordinator agent + coordinator = workflows_mistralai.Agent( + model="mistral-medium-latest", + name="deal-coordinator", + description="Coordinates deal analysis", + instructions="Analyze the deal request and hand off to specialists.", + handoffs=[risk_agent], + ) + + outputs = await workflows_mistralai.Runner.run( + agent=coordinator, + inputs=deal_request, + session=session, + ) + + analysis = "\n".join([ + output.text for output in outputs + if isinstance(output, mistralai.TextChunk) + ]) + + return {"analysis": analysis} + +if __name__ == "__main__": + asyncio.run(workflows.run_worker([DealAnalysisWorkflow])) +``` + +## Best Practices + +1. **Use RemoteSession for production** - It provides better reliability and Agents SDK integration +2. **Keep activities granular** - Small, focused activities work better as agent tools +3. **Provide clear instructions** - Agent performance depends on clear instructions +4. **Use handoffs for specialization** - Create specialized agents for specific domains and improve context management by delegating tasks +5. **Handle tool errors gracefully** - Activities used as tools should return meaningful error messages diff --git a/.claude/skills/workflows/references/guides/error-codes.mdx b/.claude/skills/workflows/references/guides/error-codes.mdx new file mode 100644 index 000000000..7484dffb1 --- /dev/null +++ b/.claude/skills/workflows/references/guides/error-codes.mdx @@ -0,0 +1,219 @@ +--- +id: error-codes +title: API Error Codes +sidebar_position: 3 +--- + +# API Error Codes + +When a request to the Workflows API fails, the response includes a structured error code in `WF_XXXX` format: + +```json +{ + "detail": "Workflow execution not found", + "code": "WF_1100" +} +``` + +For **4xx** errors, `detail` contains a specific message describing what went wrong. For **5xx** errors, `detail` is always `"An error occurred"` — the actual cause is logged server-side. + +## Quick Lookup + +| Code | Name | HTTP Status | Category | +|------|------|-------------|----------| +| [WF_1000](#wf_1000) | Unknown Error | 500 | General | +| [WF_1001](#wf_1001) | Invalid Request | 422 | General | +| [WF_1100](#wf_1100) | Workflow Not Found | 404 | Workflow | +| [WF_1101](#wf_1101) | Workflow Already Started | 409 | Workflow | +| [WF_1102](#wf_1102) | Workflow Not Running | 409 | Workflow | +| [WF_1103](#wf_1103) | Temporal Request Failed | 500 / 504 | Workflow | +| [WF_1104](#wf_1104) | Workflow Registration Failed | 500 | Workflow | +| [WF_1105](#wf_1105) | Execution Not Found | 500 | Workflow | +| [WF_1200](#wf_1200) | Schedule Failed | 422 / 500 / 503 | Schedule | +| [WF_1201](#wf_1201) | Schedule Not Found | 404 | Schedule | +| [WF_1300](#wf_1300) | Temporal Client Creation Failed | 500 | Temporal | +| [WF_1301](#wf_1301) | Temporal Initialization Failed | 500 | Temporal | +| [WF_1302](#wf_1302) | Temporal Payload Encoding Required | 500 | Temporal | +| [WF_1400](#wf_1400) | NATS Error | 500 | Streaming | +| [WF_1500](#wf_1500) | Trace Error | 500 | Tracing | +| [WF_1600](#wf_1600) | Event Store Error | 500 | Event Store | + +--- + +## General Errors (1000–1099) + +### WF_1000 + +**Unknown Error** · `500 Internal Server Error` + +A catch-all for errors that don't map to a specific code. If you see this, check server logs or contact the platform team. + +### WF_1001 + +**Invalid Request** · `422 Unprocessable Entity` + +The request was well-formed but contained invalid data. Currently returned when providing an invalid event ID during a workflow reset — the response includes a `valid_reset_events` field listing valid alternatives. + +```json +{ + "detail": "Invalid event ID for reset", + "code": "WF_1001", + "valid_reset_events": [3, 7, 12] +} +``` + +--- + +## Workflow Errors (1100–1199) + +### WF_1100 + +**Workflow Not Found** · `404 Not Found` + +The workflow execution does not exist in Temporal. This usually means the execution ID is wrong or the workflow has been purged. + +**Resolution:** Verify the execution ID. Check the [Workflows Dashboard](https://workflows.mistral.ai) for the correct ID. + +### WF_1101 + +**Workflow Already Started** · `409 Conflict` + +A workflow execution with the same ID is already running. Workflow IDs must be unique within a namespace. + +**Resolution:** Use a different execution ID, or wait for the existing execution to complete. If you need to restart it, terminate the running execution first. + +### WF_1102 + +**Workflow Not Running** · `409 Conflict` + +You attempted an action (signal, query, update, or terminate) on a workflow that is no longer running. The response includes the current workflow status. + +```json +{ + "detail": "Workflow not running", + "code": "WF_1102", + "status": "COMPLETED" +} +``` + +**Resolution:** Check the workflow's current status. If it completed or failed, start a new execution instead. + +### WF_1103 + +**Temporal Request Failed** · `500 Internal Server Error` or `504 Gateway Timeout` + +The underlying Temporal RPC call failed. This covers connection issues, timeouts, and unexpected Temporal errors. A `504` specifically indicates a deadline exceeded (the operation took too long). + +**Resolution:** Retry the request. If the issue persists, check Temporal cluster health. + +### WF_1104 + +**Workflow Registration Failed** · `500 Internal Server Error` + +Worker registration failed — the system could not register workflow definitions. This typically indicates a mismatch between workflow specs and version identifiers during worker startup. + +**Resolution:** Check worker logs for registration errors. Verify that workflow definitions are valid and that the worker can reach the API. + +### WF_1105 + +**Execution Not Found** · `500 Internal Server Error` + +An execution that was expected to exist could not be found. Unlike `WF_1100` (which is a 404 for user-facing lookups), this indicates an internal inconsistency — the execution should have existed at this point in the flow. + +**Resolution:** This is an internal error. If it persists, report it to the platform team. + +--- + +## Schedule Errors (1200–1299) + +### WF_1200 + +**Schedule Failed** · `422` / `500` / `503` + +Schedule creation or management failed. The HTTP status varies by cause: + +| Status | Cause | +|--------|-------| +| **422** | Invalid schedule specification (bad cron expression, invalid arguments) or attempting to schedule a workflow with offloaded payloads | +| **503** | Search attributes not yet available for the namespace — a transient error during namespace setup | +| **500** | Other internal failures during schedule creation or deletion | + +**Resolution:** For `422`, check your cron expression and input payload size. For `503`, retry after a few seconds. For `500`, check server logs. + +### WF_1201 + +**Schedule Not Found** · `404 Not Found` + +The schedule ID does not exist. It may have already been deleted or never created. + +**Resolution:** List existing schedules to find the correct ID. + +--- + +## Temporal Errors (1300–1399) + +### WF_1300 + +**Temporal Client Creation Failed** · `500 Internal Server Error` + +The system failed to construct a Temporal client, usually due to configuration issues (bad address, invalid credentials). + +**Resolution:** Check Temporal connection configuration (host, port, TLS settings, namespace). + +### WF_1301 + +**Temporal Initialization Failed** · `500 Internal Server Error` + +Namespace creation or search attribute registration failed during Temporal initialization. This happens during system startup. + +**Resolution:** Verify Temporal cluster health and that the namespace configuration is valid. + +### WF_1302 + +**Temporal Payload Encoding Required** · `500 Internal Server Error` + +Payload encoding is enforced but the input data was not encoded. This occurs in deployments where `temporal.enforce_payload_encoding` is enabled (typically hybrid deployments where data must stay encrypted). + +**Resolution:** Ensure your client is using the payload codec. See [Payload Encoding](../appendices/payload-encoding) for details. + +--- + +## Streaming Errors (1400–1499) + +### WF_1400 + +**NATS Error** · `500 Internal Server Error` + +A NATS operation failed — connection, publishing, or reading from a stream. + +**Resolution:** Check NATS connectivity and that `NATS_ENABLED=true` and `NATS_HOST` are set correctly. See [Streaming](streaming) for configuration details. + +--- + +## Tracing Errors (1500–1599) + +### WF_1500 + +**Trace Error** · `500 Internal Server Error` + +Failed to retrieve or parse traces from the Tempo backend. This can mean missing spans, malformed trace data, or connectivity issues with the trace provider. + +**Resolution:** Check Tempo/tracing backend health. If traces are missing, the workflow may not have emitted spans yet — wait and retry. + +--- + +## Event Store Errors (1600–1699) + +### WF_1600 + +**Event Store Error** · `500 Internal Server Error` + +A database consistency error in the event store, typically when an expected event cannot be found after a conflict. + +**Resolution:** This is an internal error. Retry the operation. If it persists, report it to the platform team. + +--- + +## SDK Error Codes + +The Python SDK (`mistralai-workflows`) has its own set of error codes for worker-side errors (invalid workflow definitions, activity configuration issues, etc.). These are different from the API error codes above and are documented in the [Workflows Exception](workflows-exception) guide. diff --git a/.claude/skills/workflows/references/guides/handling-large-data.mdx b/.claude/skills/workflows/references/guides/handling-large-data.mdx new file mode 100644 index 000000000..f1f4cbecd --- /dev/null +++ b/.claude/skills/workflows/references/guides/handling-large-data.mdx @@ -0,0 +1,140 @@ +--- +id: ha +title: How to handle large data +sidebar_position: 9 +--- + +# Handling Large Payloads with Offloaded Attributes in Workflows + +This guide explains how to use the **offloaded attribute** feature in Workflows to handle large payloads efficiently between activities, especially when dealing with Temporal's 2MB payload limit. + +## **Why Use Offloaded Attributes?** + +Temporal enforces a **2MB payload limit** for workflows and activities. To work around this, Workflows allows you to **offload large payloads** to blob storage. This ensures that your activities can handle large data without hitting Temporal's size restrictions. + +Offloaded attributes is the preferred way to handle large payloads in your activities. +Note that this differs from payload offloading which makes the payload available within the workflow itself, but is far less efficient. + +**Key Points:** + +- Offloaded values are **not accessible** in the workflow context (e.g., during workflow runs, signals, updates, or queries). +- Upload and download of offloaded data happen **within the activity context**, so timeouts are governed by the activity's own timeout settings. +- Each `OffloadableField` is stored as a separate blob. For performance, **group related fields** in a single Pydantic class if they are always used together. + +## Prerequisites + +To use offloaded attributes, you need to install the Workflows SDK with the appropriate cloud storage provider: + +```bash +## For AWS S3 +uv add "mistralai-workflows[s3]" + +## For Azure Blob Storage +uv add "mistralai-workflows[azure]" + +## For Google Cloud Storage +uv add "mistralai-workflows[gcs]" + +## Or install all storage providers +uv add "mistralai-workflows[storage]" +``` + +See the [installation guide](../getting-started/installation#cloud-storage-providers) for more details. + +## Config + +:::info +Blob storage bucket/container must have an **expiry policy** as Workflows does not automatically delete payloads. + +Align the expiry with your Temporal workflow retention period (default: 30 days). + +Blobs are prefixed by `temporal-activity-payload/`, allowing per-namespace policies. +::: + +If you use the **starter-app**, use these environment variables to configure blob storage: + +``` +## Azure: +ACTIVITY_ATTRIBUTES_OFFLOADING__ENABLED=true +ACTIVITY_ATTRIBUTES_OFFLOADING__MIN_SIZE_BYTES=1048576 # 1MB +ACTIVITY_ATTRIBUTES_OFFLOADING__STORAGE_CONFIG__STORAGE_PROVIDER=azure +ACTIVITY_ATTRIBUTES_OFFLOADING__STORAGE_CONFIG__CONTAINER_NAME=XXX +ACTIVITY_ATTRIBUTES_OFFLOADING__STORAGE_CONFIG__AZURE_CONNECTION_STRING=XXX +``` + +## **How to Define Offloadable Fields** + +To define a field as offloadable, use the `OffloadableField` type in your Pydantic model: + +```python +from mistralai.workflows.core.encoding.fields_offloader import OffloadableModel, OffloadableField + +class MyPayload(OffloadableModel): + random_field: str # Regular field (not offloaded) + large_field: OffloadableField[str] = OffloadableField(value="Hello World") # Offloadable +``` + +## **Using Offloadable Fields in Activities** + +In your activity, access the offloaded value using the `get_value()` method. +The system handles offloading and restoration automatically: + +```python +@workflows.activity(display_name="Append string to file") +async def my_activity(params: MyPayload) -> MyPayload: + # Access the offloaded value using get_value() + logger.info("Large field value", value=params.large_field.get_value()) + + # Return a new payload with the updated value + return MyPayload( + random_field="activity_return", + large_field=OffloadableField(value=params.large_field.get_value() + "_updated") + ) +``` + +## **Using Offloadable Fields in Workflows** + +In the workflow context, **do not** access the `get_value()` method of offloaded fields. Instead, pass the `OffloadableField` object directly to the next activity: + +```python +@workflows.workflow.define(name="sub-workflow") +def my_workflow(...): + # Call an activity with an offloadable field + activity_result = await my_activity( + MyPayload( + random_field="activity_param", + large_field=OffloadableField(value="initial_small_value") + ) + ) + + # Pass the offloaded field to the next activity + next_activity_params = MyPayload( + random_field="second_activity_param", + # OK: Pass the OffloadableField object directly + large_field=activity_result.large_field + ) + + # DO NOT: Try to access the value in the workflow context as it could be offloaded + # large_field=OffloadableField(value=activity_result.large_field.get_value()) + + await my_activity(next_activity_params) +``` + +## **Performance Optimization: Grouping Offloadable Fields** + +Each `OffloadableField` is stored as a **separate blob** in blob storage. To optimize performance, especially when multiple fields are always used together, **group them into a single Pydantic class**. This reduces the number of blob storage operations and improves efficiency. + +**Example:** + +```python +from mistralai.workflows.core.encoding.fields_offloader import OffloadableModel, OffloadableField +from pydantic import BaseModel + +class LargeDataGroup(BaseModel): + field1: str + field2: bytes + field3: dict[str, str] + +class MyPayload(OffloadableModel): + large_data: OffloadableField[LargeDataGroup] # Single offloaded group +``` diff --git a/.claude/skills/workflows/references/guides/limitations.mdx b/.claude/skills/workflows/references/guides/limitations.mdx new file mode 100644 index 000000000..4876c7079 --- /dev/null +++ b/.claude/skills/workflows/references/guides/limitations.mdx @@ -0,0 +1,255 @@ +--- +id: limitations +title: System Limitations +sidebar_position: 3 +--- + +# Limitations + +This document outlines the key constraints and requirements when working with workflows and activities in the system. Understanding these limitations helps you design robust and efficient applications. + +## Workflow Limitations + +Workflows should focus on orchestration logic, keeping operations deterministic and fast (under 2 seconds between activities), while delegating all I/O, non-deterministic operations, and heavy computations to activities. + +### Timeout of 2 seconds between activities + +Workflows must complete CPU-intensive operations within 2 seconds between activity invocations. This means: + +- Workflow code itself must execute quickly +- Heavy computations should be moved to activities (with appropriate `start_to_close_timeout`, [see more](#set-appropriate-timeouts)) +- The 2-second limit applies to the workflow's processing time between activity calls + +_Why?_ Workflows are designed for orchestration rather than heavy computation. The 2-second limit ensures quick recovery/resume from the last checkpoint, as the system creates checkpoints at each activity call. + +**Operations that should be moved to activities** (not done in workflows): + +- Large data validation + +```python +## ❌ Wrong - Should be in an activity +Dataset.model_validate(large_amount_of_data) +``` + +- Complex computations + +```python +## ❌ Wrong - Should be in an activity +sum([x for x in range(1000000)]) +``` + +- Cryptographic operations + +```python +## ❌ Wrong - Should be in an activity +hashlib.sha256(large_data).hexdigest() +``` + +- Heavy data transformations + +```python +## ❌ Wrong - Should be in an activity +map(lambda x: x * 2, large_list) +``` + +- Any operation that might exceed 2 seconds of CPU time + +### No I/O operations + +**All I/O operations must be performed in activities.** This includes: + +- Network calls (HTTP, gRPC, database queries) + +```python +## ❌ Wrong - Should be in an activity +await conn.execute("SELECT * FROM users") +``` + +- File system operations + +```python +## ❌ Wrong - Should be in an activity +async with aiofiles.open("file.txt", mode="r") as f: + contents = await f.read() +``` + +- Any external system interactions + +```python +## ❌ Wrong - Should be in an activity +await client.send_message("Hello") +``` + +_Why?_ Workflow code may be replayed during recovery, and I/O operations would violate determinism requirements. + +### Allowed long-running operations + +Workflows can safely perform these operations that don't block execution: + +- Activity execution (which can run much longer, [see Activity Limitations](#activity-limitations)) + ```python + # ✅ Correct - Activity can run for hours + result = await my_activity(...) + ``` +- Waiting for conditions + ```python + # ✅ Correct - Can wait for years + await workflows.workflow.wait_condition(lambda: self.processing_complete) + ``` +- Timed delays + ```python + # ✅ Correct - Can sleep for years + await asyncio.sleep(60 * 60 * 24 * 365) + ``` +- Child workflows + ```python + # ✅ Correct - Can run for at most 1 year (see `execution_timeout`) + child_result = await workflows.workflow.execute_workflow( + workflow=MyWorkflow, + params=MyWorkflowParams(...), + execution_timeout=timedelta(days=365) + ) + ``` + +### Code must be deterministic + +Workflow code must be deterministic - given the same activity history and inputs, it must produce the same outputs. This means that any non-deterministic operations like: + +- Generating random numbers + +```python +## ❌ Wrong - Non-deterministic +if random.random() > 0.5: + ... +else: + ... +``` + +- Getting current time + +```python +## ❌ Wrong - Non-deterministic +import time + +await my_activity(time.time()) +``` + +- Reading environment variables + +```python +## ❌ Wrong - Non-deterministic +if os.getenv("ENV") == "prod": + ... +``` + +must be performed in activities. While activities themselves can produce different outputs for the same inputs (as they might contain non-deterministic operations), the workflow code must behave deterministically based on the activity results it receives. + +### Maximum Execution History + +The execution history for each workflow is capped at 51,200 events or 50MB per workflow execution. This limit is again due to temporal's current constraints. In order to avoid hitting this limit, we recommend using `workflows.execute_activities_in_parallel`, that allows you to execute large amounts of activities in parallel, while having the lowest footprint on the execution history. [Learn more about concurrency patterns](concurrency). + +## Activity Limitations + +Activities are where you should perform all non-deterministic, I/O-bound, or CPU-intensive operations. Here's how to properly structure activity code: + +### Perform all I/O operations + +Activities are the proper place for all I/O operations, including: + +- Database queries + +```python +@workflows.activity() +async def query_database( + params: QueryParams, + conn: asyncpg.Connection = Depends(get_db_connection), +) -> QueryResult: + # ✅ Correct - I/O in activity + return await conn.fetch(params.query) +``` + +- API calls + +```python +@workflows.activity() +async def call_external_api(params: ApiParams) -> ApiResponse: + # ✅ Correct - network I/O in activity + async with httpx.AsyncClient() as client: + response = await client.post(params.url, json=params.data) + return ApiResponse(data=response.json()) +``` + +- File operations + +```python +@workflows.activity() +async def process_file(params: FileParams) -> FileResult: + # ✅ Correct - file I/O in activity + async with aiofiles.open(params.path, mode='r') as f: + contents = await f.read() + return FileResult(content=contents) +``` + +### Handle CPU-intensive operations + +Move all heavy computations to activities: + +```python +@workflows.activity() +def process_large_dataset(params: DatasetParams) -> ProcessedResult: + # ✅ Correct - heavy computation in activity + large_dataset = await load_large_dataset(params.dataset_id) + processed = [transform(x) for x in large_dataset] + return ProcessedResult(data=processed) +``` + +### Use non-deterministic operations + +Activities can safely use non-deterministic operations: + +```python +@workflows.activity() +def get_current_time_with_id(params: TimeParams) -> TimeResult: + # ✅ Correct - non-deterministic operations in activity + current_time = datetime.now().isoformat() + random_id = str(uuid.uuid4()) + return TimeResult(id=random_id, timestamp=current_time, timezone=params.timezone) +``` + +### Set appropriate timeouts + +Configure timeouts based on operation characteristics: + +```python +@workflows.activity(start_to_close_timeout=timedelta(hours=1)) +async def long_running_process(params: ProcessParams) -> ProcessResult: + # ✅ Correct - long operation with appropriate timeout + result = await perform_complex_operation(params) + return ProcessResult(data=result) +``` + +### Asynchronous Requirement + +**All your code must be asynchronous or extremely CPU-bound.** This means: + +1. **For I/O operations (network calls, file operations, etc.), always use async libraries:** + + ✅ **Do use:** + + - `httpx.AsyncClient` or `aiohttp` instead of synchronous `requests` + - `aiofiles` instead of `open()` for file operations + - Async database drivers (e.g., `sqlalchemy.ext.asyncio`) + + ❌ **Don’t use:** + + - Synchronous `requests` for HTTP calls + - Blocking `open()` for file operations + - Synchronous database drivers (e.g., `sqlalchemy`) + +2. For CPU-bound operations that cannot be made async, ensure they're implemented in activities with appropriate timeouts. + +## Additional Considerations + +Most limitations stem from Temporal's underlying architecture. For comprehensive details, refer to the [official Temporal documentation](https://docs.temporal.io/workflow-execution/limits). + +When designing workflows, remember that workflow code is replayed during recovery, so keep logic simple and deterministic, while moving complex operations and all I/O to activities. diff --git a/.claude/skills/workflows/references/guides/local-execution.mdx b/.claude/skills/workflows/references/guides/local-execution.mdx new file mode 100644 index 000000000..8755c921a --- /dev/null +++ b/.claude/skills/workflows/references/guides/local-execution.mdx @@ -0,0 +1,172 @@ +--- +id: local-execution +title: Local Execution +sidebar_position: 10 +--- + +# Local Execution + +Run workflows and activities directly without infrastructure. Useful for quick iteration during development. + +## Why Use Local Execution + +Local execution enables running workflow code without deploying infrastructure: + +- **Rapid prototyping** - Iterate on activity/workflow logic before infrastructure is ready +- **Pure logic validation** - Verify business logic in isolation +- **CI checks** - Run lightweight tests that don't need durability guarantees + +:::warning +Local execution is limited to prototyping. It cannot replicate distributed capabilities like durability, streaming, or human-in-the-loop patterns. Move to full deployment as soon as possible. +::: + +## Executing Workflows Locally + +Use `workflows.execute_workflow()` to run workflows locally: + +```python +import mistralai.workflows as workflows +from pydantic import BaseModel + +@workflows.activity() +async def validate_data(data_id: str) -> bool: + return True + +@workflows.activity() +async def process_data(data_id: str) -> dict: + return {"processed_id": data_id, "status": "done"} + +class DataProcessingParams(BaseModel): + data_id: str + validate_first: bool = True + +@workflows.workflow.define(name="data-processing") +class DataProcessingWorkflow: + @workflows.workflow.entrypoint + async def run(self, data_id: str, validate_first: bool = True) -> dict: + if validate_first and not await validate_data(data_id): + raise ValueError("Validation failed") + return await process_data(data_id) + +## Local execution — params must be a Pydantic model, not a dict +result = await workflows.execute_workflow( + DataProcessingWorkflow, + params=DataProcessingParams(data_id="item-123", validate_first=True) +) +``` + +### Nested Workflows + +Child workflows via `workflows.execute_workflow()` execute directly in local mode: + +```python +import mistralai.workflows as workflows +from pydantic import BaseModel + +class ChildParams(BaseModel): + value: str + +class ParentParams(BaseModel): + data: str + +@workflows.workflow.define(name="child") +class ChildWorkflow: + @workflows.workflow.entrypoint + async def run(self, value: str) -> str: + return f"Child: {value}" + +@workflows.workflow.define(name="parent") +class ParentWorkflow: + @workflows.workflow.entrypoint + async def run(self, data: str) -> str: + child_result = await workflows.execute_workflow( + ChildWorkflow, params=ChildParams(value=data) + ) + return f"Parent got: {child_result}" + +## Full chain executes locally +result = await workflows.execute_workflow(ParentWorkflow, params=ParentParams(data="test")) +``` + +## Executing Activities Locally + +Activities called outside workflows execute directly with: + +- **Automatic retries** via tenacity (respects `retry_policy_max_attempts`, `retry_policy_backoff_coefficient`) +- **Dependency injection** - `Depends()` resolved automatically +- **Type validation** - Input/output types are validated +- **Nested calls** - Only outermost activity applies retry logic + +```python +import mistralai.workflows as workflows +from mistralai.workflows import Depends + +class HttpClient: + def __init__(self, timeout: int): + self.timeout = timeout + + async def get(self, path: str) -> dict: + return {"id": "123", "name": "Test User"} + +async def get_client() -> HttpClient: + return HttpClient(timeout=30) + +@workflows.activity(retry_policy_max_attempts=3) +async def fetch_user( + user_id: str, + client: HttpClient = Depends(get_client) +) -> dict: + return await client.get(f"/users/{user_id}") + +## Retries and DI work locally +user = await fetch_user("123") +``` + +## Tradeoffs: What You Lose + +Local execution cannot replicate distributed capabilities: + +| Feature | Local | Full Deployment | Impact | +| ---------------------------- | ---------------- | ------------------- | -------------------------------------- | +| **Durability** | ❌ None | ✅ Full history | Crash = lost state, no replay | +| **Streaming** | ❌ No events | ✅ Real-time events | No `Task` events, no progress tracking | +| **HITL (Human-in-the-Loop)** | ❌ Not supported | ✅ Signals/Updates | Cannot pause for human input | +| **Signals** | ❌ | ✅ | No external event injection | +| **Queries** | ❌ | ✅ | Cannot inspect running state | +| **Updates** | ❌ | ✅ | No synchronous state mutations | +| **Scheduling** | ❌ | ✅ | No cron/interval triggers | +| **`wait_condition`** | ❌ Throws | ✅ Works | Workflow primitives fail locally | +| **Failure Recovery** | ❌ | ✅ | No automatic checkpoint resume | +| **Event History** | ❌ | ✅ | No UI debugging | +| **Rate Limiting** | ⚠️ Local only | ✅ Distributed | No cross-worker coordination | +| **Worker Stickiness** | ⚠️ No-op | ✅ Works | Always same "worker" | + +### Critical Limitations + +**No Streaming/Observability**: The `Task` system emits events to the Workflows API. Locally, `should_publish_event()` returns false—no events are published. Progress tracking, custom task states, and real-time updates don't work. + +**No Human-in-the-Loop**: Patterns relying on signals (`workflow.signal`), updates (`workflow.update`), or external triggers cannot run locally. The workflow runs to completion without pause points. + +**No Timeout Enforcement**: `start_to_close_timeout` is respected but not enforced at system level. Runaway activities won't be killed. + +## How It Works + +The SDK detects execution context via `temporalio.workflow.in_workflow()`. Outside a workflow context, activities and workflows execute as regular Python async functions with retry wrappers. + +```python +import mistralai.workflows as workflows + +@workflows.activity() +async def send_email(to: str, subject: str, body: str) -> dict: + return {"message_id": "msg-123"} + +## Direct call - executes locally with retry logic +result = await send_email("user@example.com", "Hello", "Test message") +``` + +## Next Steps + +- [Activities](./activities) - Activity patterns and configuration +- [Workflows](./workflows) - Workflow fundamentals +- [Signals & Queries](./signals-queries-updates) - External interaction (requires deployment) +- [Streaming](./streaming) - Real-time event streaming (requires deployment) diff --git a/.claude/skills/workflows/references/guides/migration-v2-to-v3.mdx b/.claude/skills/workflows/references/guides/migration-v2-to-v3.mdx new file mode 100644 index 000000000..49bdc89eb --- /dev/null +++ b/.claude/skills/workflows/references/guides/migration-v2-to-v3.mdx @@ -0,0 +1,213 @@ +# Migrating from v2.0 to v3.0 + +## Package Namespace Migration + +The SDK package namespace has changed from `mistralai_workflows` to `mistralai.workflows`. Update all imports: + +```python +# ❌ OLD +from mistralai_workflows import workflow, activity, run_worker +from mistralai_workflows.task import task + +# ✅ NEW +from mistralai.workflows import workflow, activity, run_worker +from mistralai.workflows.task import task +``` + +This applies to all submodules (plugins, models, core, etc.). + +## Removed Deprecated APIs + +The following deprecated APIs have been removed. Code using them will fail at import or runtime. + +| Removed | Replacement | +|---------|-------------| +| `@activity(activity_name="...")` | `@activity(name="...")` | +| `@activity(display_name="...")` | Remove (no replacement) | +| `@activity(_extends=...)` | Remove (singledispatch extension mechanism removed) | +| `@workflow.define(workflow_name="...")` | `@workflow.define(name="...")` (`name` is now a required positional argument) | +| `record_event()` | Use the `task` context manager from `mistralai.workflows.task` | +| `record_event_progress()` | Use the `task` context manager | +| `record_event_progress_function()` | Use the `task` context manager | + +### `@workflow.define` — `name` is now required + +```python +# ❌ OLD +@workflow.define(workflow_name="my-workflow") + +# ❌ ALSO BROKEN (name was optional before) +@workflow.define() + +# ✅ NEW +@workflow.define("my-workflow") +# or +@workflow.define(name="my-workflow") +``` + +## Default Execution Timeout + +Workflow executions now have a **default timeout of 1 hour**. Previously there was no default, which could cause infinite retries when no workers were active. + +If your workflows run longer than 1 hour, set an explicit timeout: + +```python +@workflow.define("my-long-workflow", execution_timeout=timedelta(hours=4)) +``` + +This also applies to child workflows: + +```python +await execute_workflow(MyChildWorkflow, params, execution_timeout=timedelta(hours=2)) +``` + +## Determinism Enforcement Enabled by Default + +The Temporal sandbox is now enabled by default to catch non-deterministic workflow code (e.g., using `datetime.now()`, `random`, or unrestricted imports in workflow methods). + +If your workflows are not yet determinism-safe, you can disable enforcement: + +**Per-workflow:** +```python +@workflow.define("my-workflow", enforce_determinism=False) +``` + +**Globally (environment variable):** +```bash +DEFAULT_ENFORCE_DETERMINISM=0 +``` + +For workflows that need to opt out of the sandbox for specific imports, use the re-exported Temporal utilities on `workflow.unsafe`: +```python +from mistralai.workflows import workflow + +with workflow.unsafe.imports_passed_through(): + import some_non_deterministic_module +``` + +## Child Workflow `execution_timeout` Fix + +The `execution_timeout` parameter on `execute_workflow()` was previously incorrectly passed as `task_timeout` to Temporal, meaning it limited a single task rather than the full execution. This is now fixed — `execution_timeout` controls the total execution time including retries and continue-as-new. + +**Action required:** If you were relying on the old (incorrect) behavior where `execution_timeout` acted as a per-task timeout, adjust your timeout values accordingly. + +## `WorkflowVersion` Renamed to `WorkflowRegistration` + +The `WorkflowVersion` model has been renamed to `WorkflowRegistration` across the SDK. The server accepts both old and new field names during a transition period, but SDK code should be updated: + +| v2 | v3 | +|----|-----| +| `WorkflowVersion` | `WorkflowRegistration` | +| `workflow_version_id` | `workflow_registration_id` | +| `workflow_version_ids` | `workflow_registration_ids` | + +## Deployment-Based Task Queue Routing + +Workers now use `DEPLOYMENT_NAME` as the Temporal task queue name. The old `TEMPORAL_TASK_QUEUE` environment variable is kept for backwards compatibility but raises an error if it conflicts with `DEPLOYMENT_NAME`. + +**Versioned workers** (with `deployment_name` and `build_id`) are forbidden from using the `default` task queue to prevent Temporal versioning conflicts. + +--- + + +## `WorkflowsClient` Removed + +`WorkflowsClient` has been removed entirely. All workflow operations now go through the Mistral SDK client (`mistral_client`). + +### Creating a client + +```python +# ❌ OLD +from mistralai.workflows import WorkflowsClient + +client = WorkflowsClient( + base_url="https://api.mistral.ai", + api_version="v1", + api_key="your-key", +) + +# ✅ NEW +from mistralai.workflows.client import get_mistral_client + +mistral_client = get_mistral_client() +``` + +### Executing a workflow + +```python +# ❌ OLD +async with WorkflowsClient(base_url=..., api_key=...) as client: + await client.execute_workflow( + workflow_identifier="my-workflow", + input_data=params, + execution_id=exec_id, + task_queue=task_queue, + ) + +# ✅ NEW +mistral_client = get_mistral_client() +await mistral_client.workflows.execute_workflow_async( + workflow_identifier="my-workflow", + input=params.model_dump(mode="json"), + execution_id=exec_id, + task_queue=task_queue, +) +``` + +### Waiting for workflow completion + +```python +# ❌ OLD +result = await client.execute_workflow_and_wait( + workflow_identifier="my-workflow", + input_data=params, +) + +# ✅ NEW +result = await mistral_client.workflows.execute_workflow_and_wait_async( + workflow_identifier="my-workflow", + input=params.model_dump(mode="json"), +) +``` + +### Scheduling a workflow + +```python +# ❌ OLD +await client.schedule_workflow( + schedule=schedule_def, + workflow_identifier="my-workflow", +) + +# ✅ NEW +await mistral_client.workflows.schedule_workflow_async( + schedule=schedule_def.model_dump(mode="json"), + workflow_identifier="my-workflow", +) +``` + +### Streaming workflow events + +```python +# ❌ OLD +async for event in client.stream_events(execution_id=exec_id): + ... + +# ✅ NEW +from mistralai.workflows.client import stream_workflow_events + +async for event in stream_workflow_events(mistral_client, execution_id=exec_id): + ... +``` + +### Import changes + +| v2 | v3 | +|----|-----| +| `from mistralai.workflows import WorkflowsClient` | `from mistralai.workflows.client import get_mistral_client` | +| `WorkflowsClient(base_url=..., api_key=...)` | `get_mistral_client(server_url=..., api_key=...)` | +| `client.execute_workflow(...)` | `mistral_client.workflows.execute_workflow_async(...)` | +| `client.execute_workflow_and_wait(...)` | `mistral_client.workflows.execute_workflow_and_wait_async(...)` | +| `client.schedule_workflow(...)` | `mistral_client.workflows.schedule_workflow_async(...)` | +| `client.stream_events(...)` | `stream_workflow_events(mistral_client, ...)` | + diff --git a/.claude/skills/workflows/references/guides/observability.mdx b/.claude/skills/workflows/references/guides/observability.mdx new file mode 100644 index 000000000..e3cf0fcf8 --- /dev/null +++ b/.claude/skills/workflows/references/guides/observability.mdx @@ -0,0 +1,45 @@ +--- +id: observabilities +title: Workflow Observability +sidebar_position: 6 +--- + +# Workflow Observability + +This guide focuses on OpenTelemetry traces for execution-level diagnostics. If you need custom lifecycle and progress events emitted into workflow history for external consumers, see the [Task Events](../task-events) guide. + +## Traces (OpenTelemetry) + +Traces capture execution details (spans, timings, errors) and are optimized for debugging and performance analysis. They are independent from custom task events. + +### Activity Observability + +Activities automatically generate spans. Use the `name` parameter to make them more readable: + +```python +@workflows.activity(name="Processing customer emails") +async def process_emails(params: ActivityParams) -> ActivityResult: + # Your activity code +``` + +### Trace Sampling + +Trace collection is sampled. By default the worker uses a parent-based sampler with a configurable sample rate, so upstream decisions can be honored. + +**TL;DR:** For the finest control of sampling, pass a `traceparent` header at your workflow entry point (or API edge). This lets you force sampling on or off and propagate the parent trace consistently. + +### Fetching Workflow Traces + +```python +## Get trace data +trace = await client.workflows.executions.get_workflow_execution_trace_otel_async(execution_id) + +## Get trace summary +summary = await client.workflows.executions.get_workflow_execution_trace_summary_async(execution_id) + +## Get detailed events +events = await client.workflows.executions.get_workflow_execution_trace_events_async( + execution_id, + include_internal_events=False # Hide system events +) +``` diff --git a/.claude/skills/workflows/references/guides/payload-encoding.mdx b/.claude/skills/workflows/references/guides/payload-encoding.mdx new file mode 100644 index 000000000..b1c59955b --- /dev/null +++ b/.claude/skills/workflows/references/guides/payload-encoding.mdx @@ -0,0 +1,251 @@ +--- +id: payload-encoding +title: Payload encoding +sidebar_position: 4 +--- + +# Payload Encoding + +## Overview + +The **payload encoding process** enables seamless processing of payloads before they are sent to the Temporal server and after they are received. This process leverages **Temporal payload converters, codecs, and interceptors** to provide a transparent experience for developers. + +### Key Features + +- **Payload Offloading**: Temporal imposes a 2MB limit on payload size. The encoding process automatically offloads large payloads to a blob storage service and retrieves them during decoding. +- **Encryption/Decryption**: Payloads can be encrypted before being sent to the Orchestration Layer and decrypted by the worker allowing data to stay in the user’s infrastructure even in hybrid deployments. + +### Architecture Context + +Mistral workflows are designed to operate in a **hybrid mode**: + +- The **orchestration layer (Workflows API)** runs on Mistral infrastructure. +- The **code (workers/clients)** runs in the user’s infrastructure. + +To ensure data remains within the user’s infrastructure, the encoding/decoding process is implemented in the **Workflows SDK** and not in the Workflows API. + +## Architecture + +### PayloadEncoder + +The **PayloadEncoder** (`mistralai/workflows/worker`) is the core component for payload encoding/decoding. +It manages offloading, encryption, and other payload transformations, and is used by both the and **Temporal Worker**. + +### Temporal Components + +To allow a seemless integration, Mistral Workflows rely on three Temporal key components: **Payload Converter**, **Codec**, and **Interceptor**. + + + +| Component | Description | Constraints | +| --------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------- | +| **Payload Converter** | Converts between application-specific payloads and Temporal’s internal format. | No access to workflow context. Must use deterministic code. | +| **Codec** | Encodes/decodes payloads (serialization, compression, encryption). | No access to workflow context. | +| **Interceptor** | Intercepts payloads before/after Temporal server communication. | Can access workflow context. Must use deterministic code. | + +### Workflow + +The diagram below illustrates the end-to-end flow of payload encoding, from the client SDK to the Temporal server and back to the worker. It highlights the key steps: encoding/offloading, API orchestration, and decoding/context propagation. + +![Payload encoding design diagram](/img/payload-encoding-scheme.svg) + +--- + +#### 1. API Client (User Side / Application Layer) + +The user start a workflow using the execute_workflow method of the API Client. + +#### `execute_workflow` Process: + +1. **Encode the payload** using `PayloadEncoder`: + - Offloads large payloads to blob storage. + - _(Future: Encrypts the payload.)_ +2. **Transform the payload** into a `NetworkEncodedInput` (base64-encoded string with encoding options). +3. **Call the Workflows API** with the `NetworkEncodedInput` to start the workflow. + +#### `NetworkEncodedInput` Structure: + +```json +{ + "b64payload": "", + "encoding_options": ["BLOB_STORAGE", "..."], + "empty": false +} +``` + +--- + +#### 2. Workflows API (Server Side / Orchestration Layer) + +**Receive** the `NetworkEncodedInput` from the client and forwards it to temporal. + +#### Temporal Payload structure: + +```json +{ + "metadata": { + "encoding": "json/wf_v1", + "namespace": "XXX", + "execution_id": "XXX", + "offloaded": true + }, + "data": "" +} +``` + +--- + +#### 3. Workflows Worker (User Side / Application Layer) + +1. **Receive** the Temporal `Payload` from the Temporal server. +2. **Decode the payload**: + + - **Codec (`MistralWorkflowsPayloadCodec`)**: + - Uses `PayloadEncoder` to decode the payload. + - Fetches offloaded payloads from blob storage if needed. + - _(Future: Decrypts the payload.)_ + - **Payload Converter (`MistralWorkflowsPayloadConverter`)**: + - Transforms the Temporal `Payload` into a `PayloadWithContext`. + - `PayloadWithContext` contains the decoded payload and context (e.g., `execution_id`, `namespace`). + - **Interceptor (`WorkflowContextWorkflowInboundInterceptor`)**: + - Sets the context in `contextvar` for later use when calling activities/sub-workflows. + - Unwraps the payload from `PayloadWithContext` to send only the payload content as param to the workflow code. + - Execute the workflow code with the decoded payload. + +3. **Calling Activities/Sub-Workflows**: + - The process is reversed: + - **Interceptor**: Wraps the payload in `PayloadWithContext` to forward the context. + - **Payload Converter**: Transforms `PayloadWithContext` into a Temporal `Payload`. + - **Codec**: Uses `PayloadEncoder` to encode the payload (offloads/encrypts as needed). + - **Sends** the Temporal `Payload` to the Temporal server. + - The Temporal server forwards the `Payload` to the activity/sub-workflow worker and repeat the step 3. + +## Configuration + +### Base Encoding + +Payload encoding is **always enabled** by default. This ensures proper context propagation across workflows, activities, and workers. No configuration is needed for base encoding. + +The encoding system provides: + +- Workflow context propagation (namespace, execution ID, parent workflow tracking) +- Support for streaming events with correct lineage +- Foundation for optional offloading and encryption features + +--- + +### Payload Offloading + +Payload offloading allows seamless transmission of payloads larger than 2MB by configuring a blob storage (e.g., Azure Blob Storage, S3, or GCP Cloud Storage) accessible to both client and worker code. + +**Prerequisites** + +To use payload offloading, you need to install the Workflows SDK with the appropriate cloud storage provider: + +```bash +## For AWS S3 +uv add "mistralai-workflows[s3]" + +## For Azure Blob Storage +uv add "mistralai-workflows[azure]" + +## For Google Cloud Storage +uv add "mistralai-workflows[gcs]" + +## Or install all storage providers +uv add "mistralai-workflows[storage]" +``` + +See the [installation guide](../getting-started/installation#cloud-storage-providers) for more details. + +**Limitations :warning:** + +- **Workflow Replay Overhead**: When replaying a workflow, all payloads are downloaded again. This can lead to: + - **Timeout issues** during task execution. + - **Worker stalls** while rebuilding history. +- **Not for Generic Data Exchange**: This system is **not designed** for general-purpose data exchange due to the above limitations. + +**Use Case** +This feature is intended only for edge cases where large payloads are uncommon but may occasionally occur in very rare situations. + +For a more robust approach to handling large data exchange, prefer using the [activity field offloader](../guides/handling-large-data). + +:::info +Blob storage bucket/container must have an **expiry policy** as payloads are not automatically deleted. + +Align the expiry with your Temporal workflow retention period (default: 30 days). + +Blobs are prefixed by `temporal-payload/`, allowing per-namespace policies. +::: + +If you use the **starter-app**, use these environment variables to configure blob storage: + +``` +TEMPORAL_PAYLOAD_OFFLOADING__ENABLED=true + +## Azure: +TEMPORAL_PAYLOAD_OFFLOADING__STORAGE_CONFIG__STORAGE_PROVIDER=azure +TEMPORAL_PAYLOAD_OFFLOADING__STORAGE_CONFIG__CONTAINER_NAME=abraxas-temporal-payloads +TEMPORAL_PAYLOAD_OFFLOADING__STORAGE_CONFIG__AZURE_CONNECTION_STRING="XXX" +``` + +--- + +### Encryption + +Encryption allows you to encrypt the data sent to the Mistral API and through the Temporal server. +Encrypted data will show up as **encrypted** in traces as well. + +To enable it, you must generate a cryptographic key and configure the API Client and workers to use it. +Use the following Python code to generate a 256-bit AES-GCM key: + +```python +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +## Generate a 256-bit (32-byte) key +key = AESGCM.generate_key(bit_length=256) +print(f"Generated key: {key.hex()}") +``` + +There are two modes: +`full`: Encrypts every payload. +`partial`: Encrypt only `EncryptedStrField` fields (see: `mistralai.workflows.models`) + +Set the following environment variables in your .env: + +``` +TEMPORAL_PAYLOAD_ENCRYPTION__MODE=full # or partial +TEMPORAL_PAYLOAD_ENCRYPTION__MAIN_KEY= +``` + +#### Key Rotation + +To handle key rotation, follow these steps: + +#### 1. Create a New Key + +Create a new key using the same method as above. + +#### 2. Update Your Settings + +Add the new key as the `MAIN_KEY` and keep the old key as the `SECONDARY_KEY`. This lets the system decrypt older data while using the new key for encryption. + +``` +TEMPORAL_PAYLOAD_ENCRYPTION__MODE=full +TEMPORAL_PAYLOAD_ENCRYPTION__MAIN_KEY= +TEMPORAL_PAYLOAD_ENCRYPTION__SECONDARY_KEY= +``` + +#### 3. Wait for old workflows to Finish + +Allow sufficient time (days or weeks, depending on your workflows) for all data encrypted with the old key to be fully processed. + +#### 4. Remove the old Key + +Once you're sure no old data needs decrypting, delete the `SECONDARY_KEY` from your settings: + +``` +TEMPORAL_PAYLOAD_ENCRYPTION__MODE=full +TEMPORAL_PAYLOAD_ENCRYPTION__MAIN_KEY= +## TEMPORAL_PAYLOAD_ENCRYPTION__SECONDARY_KEY= +``` diff --git a/.claude/skills/workflows/references/guides/rate-limiting.mdx b/.claude/skills/workflows/references/guides/rate-limiting.mdx new file mode 100644 index 000000000..c4e04bc0e --- /dev/null +++ b/.claude/skills/workflows/references/guides/rate-limiting.mdx @@ -0,0 +1,58 @@ +--- +id: rate-limiting +title: Rate Limiting +sidebar_position: 7 +--- + +# Rate Limiting in Workflows + +Rate limiting is a crucial aspect of workflow management that helps control resource consumption and prevent any single workflow or activity from monopolizing shared resources. + +## How Rate Limiting Works + +Rate limits are always shared across all workers and workflows in the same task workspace (or `TEMPORAL_TASK_QUEUE` if configured). The `key` parameter controls how activities share these limits: + +### Case 1: Rate Limit Per Activity (No Key Provided) + +When **no key is provided**, the rate limit applies to the activity itself and is shared across all workers and workflows that use it. + +**Use this when:** You want to protect a shared resource (like an API client) that should have a global rate limit regardless of which workflow is using it. + +### Case 2: Rate Limit Across Activities (Key Provided) + +When **a key is provided**, multiple different activities that use the same key share a single rate limit pool across all workers and workflows. + +**Use this when:** You need to coordinate rate limits across multiple different activities (e.g., limiting total API calls across read, write, and delete operations using the same external service). + +### Example: Shared Rate-Limited API Client (No Key) + +```python +import mistralai.workflows as workflows +from mistralai import Mistral, Messages, Depends +from pydantic import BaseModel + +def get_mistral_client() -> Mistral: + """Creates a shared chat completion client with rate limiting""" + client = Mistral( + api_key="your_api_key", + ) + return client + +class CompletionParams(BaseModel): + model: str + messages: Messages + +@workflows.activity(rate_limit=workflows.RateLimit(time_window_in_sec=1, max_calls=100)) +async def generate_chat_response( + params: CompletionParams, + client: Mistral = Depends(get_mistral_client) +): + """Generates a chat response using a shared client""" + # This activity can be called from multiple workflows + # but will share the same rate limit across all of them + return await client.chat.complete_async(model=params.model, messages=params.messages) +``` + +**Behavior**: All workflows calling `generate_chat_response` share the same 100 calls/sec limit. If Workflow A makes 60 calls and Workflow B makes 50 calls in the same second, they compete for the same pool. + +**With a key**: Add `key="mistral_api"` to share this limit across multiple activities (e.g., `generate_chat_response`, `generate_embeddings`, `moderate_content`). diff --git a/.claude/skills/workflows/references/guides/scheduling.mdx b/.claude/skills/workflows/references/guides/scheduling.mdx new file mode 100644 index 000000000..ee75bceb1 --- /dev/null +++ b/.claude/skills/workflows/references/guides/scheduling.mdx @@ -0,0 +1,115 @@ +--- +id: scheduling +title: Workflow Scheduling +sidebar_position: 7 +--- + +# Workflow Scheduling + +Automatically run workflows at specified times without manual triggering. + +## How It Works + +Workers automatically register workflow schedules with Workflows during startup. The system handles: + +1. Schedule registration with Workflows +2. Periodic refresh of schedule definitions (every 10 seconds) +3. Execution of workflows according to the schedule + +## Defining Schedules + +Add schedules to workflows using cron expressions: + +```python +from mistralai.workflows.models import ScheduleDefinition + +schedule = ScheduleDefinition( + input={"report_type": "daily"}, + cron_expressions=["0 0 * * *"] # Daily at midnight UTC +) + +@workflows.workflow.define(schedules=[schedule]) +class ReportWorkflow: + async def run(self, report_type: str = "daily"): + # Generate report + pass +``` + +## Defining Schedule Policies + +Customize schedule behavior with policies: + +```python +from mistralai.workflows.models import SchedulePolicy, ScheduleOverlapPolicy + +## Override default schedule policy +schedule_policy = SchedulePolicy( + catchup_window=timedelta(days=1), # Allow 1 day of catchup + overlap_policy=ScheduleOverlapPolicy.SKIP, # Skip overlapping executions +) + +schedule = ScheduleDefinition( + input={"report_type": "daily"}, + cron_expressions=["0 0 * * *"], # Daily at midnight UTC + policy=schedule_policy +) + +@workflows.workflow.define(schedules=[schedule]) +class ReportWorkflow: + async def run(self, report_type: str = "daily"): + # Generate report + pass +``` + +For more details on schedule policies, see the [Temporal SchedulePolicy Documentation](https://docs.temporal.io/schedule#policies). + +## Key Considerations + +1. **Worker Configuration**: + + - Ensure all workers have identical schedule configurations + - Mismatched configurations can cause conflicts and unexpected behavior + +2. **Schedule Definition**: + - Uses standard cron syntax + - Includes input parameters for scheduled executions + - Supports multiple cron expressions per workflow + +## Complete Example + +```python +from datetime import timedelta +import mistralai.workflows as workflows +from mistralai.workflows.models import ScheduleDefinition, SchedulePolicy, ScheduleOverlapPolicy + +## Run every Saturday at 3 AM UTC +backup_schedule = ScheduleDefinition( + input={"retention_days": 30}, + cron_expressions=["0 3 * * 6"], + policy=SchedulePolicy( + catchup_window=timedelta(days=7), + overlap_policy=ScheduleOverlapPolicy.SKIP, + ) +) + +@workflows.workflow.define(schedules=[backup_schedule]) +class DatabaseBackupWorkflow: + async def run(self, retention_days: int = 30): + print(f"Starting backup with {retention_days} day retention") + # Backup implementation here + +## Start worker with: +## asyncio.run(workflows.run_worker([DatabaseBackupWorkflow])) +``` + +## Important Notes + +- Schedules use UTC time zone by default +- Each schedule can specify different input parameters +- Workers automatically maintain schedule registrations +- Ensure consistent schedule definitions across all workers + +## Next Steps + +- [Workflow Guide](workflows) - Learn workflow fundamentals +- [Signals, Queries & Updates](signals-queries-updates) - Other workflow interaction methods diff --git a/.claude/skills/workflows/references/guides/signals-queries-updates.mdx b/.claude/skills/workflows/references/guides/signals-queries-updates.mdx new file mode 100644 index 000000000..82d841d18 --- /dev/null +++ b/.claude/skills/workflows/references/guides/signals-queries-updates.mdx @@ -0,0 +1,202 @@ +--- +id: signals-queries-updates +title: Signals, Queries & Updates +sidebar_position: 4 +--- + +# Signals, Queries & Updates: Workflow Communication Mechanisms + +These features enable interaction with running workflows from external systems. + +## Signals + +Signals allow external systems to send messages to running workflows. + +### Key Characteristics: + +- Asynchronous communication +- Can be sent at any time during workflow execution +- Workflows must explicitly handle signals +- Can carry payload data + +### Basic Example + +```python +import mistralai.workflows as workflows + +@workflows.workflow.define() +class NotificationWorkflow: + def __init__(self): + self.notifications = [] + + @workflows.workflow.signal(name="add_notification") + async def add_notification(self, message: str, priority: int = 1): + self.notifications.append(message) + print(f"Received notification: {message} (Priority: {priority})") + + @workflows.workflow.entrypoint + async def run(self): + print("Workflow started, waiting for notifications...") + while True: + await workflows.workflow.wait_condition(lambda: len(self.notifications) > 0) + print(f"Processing {len(self.notifications)} notifications") + self.notifications.clear() +``` + +## Queries + +Queries allow external systems to get the current state of a workflow. + +### Key Characteristics: + +- Synchronous communication +- Read-only operations (should not modify workflow state) +- Can be called at any time during workflow execution +- Must return quickly (not for long-running operations) + +### Basic Example + +```python +import mistralai.workflows as workflows + +@workflows.workflow.define() +class ProcessingWorkflow: + def __init__(self): + self.progress = 0.0 + self.completed = False + + @workflows.workflow.query(name="get_status") + def get_status(self) -> dict: + return { + "progress": self.progress, + "completed": self.completed + } + + @workflows.workflow.entrypoint + async def run(self): + # Simulate work + for i in range(1, 11): + self.progress = i * 10 + await asyncio.sleep(1) + self.completed = True +``` + +## Updates + +Updates allow external systems to modify workflow state and get a response. Unlike signals, updates can return values and can also execute activities. + +### Key Characteristics: + +- Synchronous communication (with response) +- Can modify workflow state +- Can return values to the caller +- Can execute activities +- More structured than signals + +### Basic Example with Activity Execution + +```python +import mistralai.workflows as workflows +import asyncio + +## Activity definition +@workflows.activity() +async def process_update_data(data: str) -> str: + # Simulate processing + await asyncio.sleep(0.5) + return f"Processed: {data.upper()}" + +@workflows.workflow.define() +class DataProcessingWorkflow: + def __init__(self): + self.current_value = "default" + + @workflows.workflow.update(name="update_data") + async def update_data(self, new_value: str) -> dict: + # Execute an activity as part of the update + processed = await process_update_data(new_value) + + # Update workflow state + old_value = self.current_value + self.current_value = processed + + return { + "success": True, + "processed_value": processed, + "message": f"Updated from '{old_value}' to '{processed}'" + } + + @workflows.workflow.entrypoint + async def run(self): + print(f"Workflow started with value: {self.current_value}") + # Workflow continues running... +``` + +## Comparison Table + +| Feature | Communication Type | Modifies State | Returns Value | Can Execute Activities | +| ------- | ------------------ | -------------- | ------------- | ---------------------- | +| Signal | Asynchronous | Yes | No | No | +| Query | Synchronous | No | Yes | No | +| Update | Synchronous | Yes | Yes | Yes | + +## Input Validation + +Signals, queries, and updates validate incoming payloads against their declared parameters. This ensures type safety and prevents unexpected data from reaching your handlers. + +### Automatic Schema Enforcement + +When you declare parameters with type annotations, the system automatically: + +- Validates incoming payloads match the expected types +- **Rejects extra fields** not declared in the handler signature +- Returns HTTP 422 (Unprocessable Entity) with descriptive error messages on validation failure + +```python +@workflows.workflow.signal(name="add_notification") +async def add_notification(self, message: str, priority: int = 1): + # Only 'message' and 'priority' fields are accepted + # Extra fields like {"message": "hi", "extra": "bad"} will be rejected + ... +``` + +### Using Pydantic Models for Complex Inputs + +For handlers with complex or nested input structures, define a Pydantic model: + +```python +import pydantic + +class Address(pydantic.BaseModel): + street: str + city: str + +class UserProfile(pydantic.BaseModel): + name: str + address: Address + +@workflows.workflow.signal(name="update_profile") +async def update_profile(self, profile: UserProfile) -> None: + self._profile = profile +``` + +When using Pydantic models, their configuration is preserved: + +- If you set `model_config = pydantic.ConfigDict(extra="allow")`, extra fields will be accepted +- If you set `model_config = pydantic.ConfigDict(extra="forbid")`, extra fields are rejected (this is the default behavior for auto-generated schemas) + +### Validation Error Responses + +When validation fails, the API returns HTTP 422 with details about what went wrong: + +```json +{ + "detail": "Invalid input: Additional properties are not allowed ('unexpected_field' was unexpected)" +} +``` + +## Next Steps + +- [Workflow Guide](workflows) - Learn more about workflow fundamentals +- [Activity Guide](activities) - Understand how activities complement workflows +- [Observability Guide](observability) - Monitor your workflow interactions diff --git a/.claude/skills/workflows/references/guides/streaming-consumption.mdx b/.claude/skills/workflows/references/guides/streaming-consumption.mdx new file mode 100644 index 000000000..822294641 --- /dev/null +++ b/.claude/skills/workflows/references/guides/streaming-consumption.mdx @@ -0,0 +1,401 @@ +--- +id: streaming-consumption +title: Consuming Streaming Events +sidebar_position: 8 +--- + +# Consuming Streaming Events + +Subscribe to real-time events from workflows and activities using the Workflows API client or direct NATS subscriptions. + +## Using the Workflows API Client + +### Basic Consumption + +```python +from mistralai.workflows.client import get_mistral_client + +async with get_mistral_client( + server_url="https://api.mistral.ai", + api_key=os.environ["MISTRAL_API_KEY"], +) as client: + # Start a workflow + execution = await client.workflows.execute_workflow_async( + workflow_identifier="my-workflow", + input=params, + ) + + # Stream all events for this execution + event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=execution.execution_id, + ) + async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None: + continue # skip heartbeat/comment SSE lines + event_data = payload.data # typed event response + event_type = event_data.event_type + print(f"[{payload.stream}] type={event_type} data={event_data}") +``` + +### Filtering by Stream Name + +Only receive events from a specific stream: + +```python +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=execution_id, + stream="token", # Only token events +) +async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None: + continue + print(payload.data) +``` + +### Detecting Completion + +Use `event_type` on the typed event data to detect workflow lifecycle events: + +```python +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=execution_id, +) +async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None or payload.data is None: + continue + + event = payload.data + event_type = event.event_type + + if event_type == "WORKFLOW_EXECUTION_COMPLETED": + print("Done!") + break + elif event_type in ("WORKFLOW_EXECUTION_FAILED", "WORKFLOW_EXECUTION_CANCELED"): + print(f"Ended: {event_type}") + break + else: + # Handle your custom events + record_event(event) +``` + +### Resume from Sequence + +If your connection drops, resume from where you left off using `start_seq`: + +```python +last_seq = 0 +while True: + try: + event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=execution_id, + start_seq=last_seq, + ) + async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None: + continue + last_seq = payload.broker_sequence + 1 + process(payload) + break # Completed normally + except ConnectionError: + await asyncio.sleep(1) + # Loop will resume from last_seq +``` + +## NATS Subject Structure + +Understanding the subject hierarchy helps you filter events precisely. + +### Subject Format + +``` +{stream_name}.{workflow_name}.{root_exec_id}.{parent_exec_id}.{exec_id}.{topic}.{scope} +``` + +| Segment | Description | Example | +| ---------------- | -------------------------------------------------------- | ---------------------------------- | +| `stream_name` | Stream prefix + namespace (e.g., `workflows_stream_*`) | `workflows_stream_customer:ws` | +| `workflow_name` | Workflow name | `my-workflow` | +| `root_exec_id` | Root workflow exec ID (top-level ancestor) | `root123` | +| `parent_exec_id` | Parent workflow exec ID (direct parent) | `parent456` | +| `exec_id` | Current workflow exec ID | `exec789` | +| `topic` | Stream name (your choice) | `token` | +| `scope` | `workflow` or `activity.*` | `workflow` | + +### Scope Values + +**Workflow events** end with `.workflow`: + +``` +...token.workflow +``` + +**Activity events** end with `.activity.{name}.{id}.{attempt}`: + +``` +...token.activity.chat_activity.1.1 +``` + +## NATS Wildcards + +NATS supports two wildcards for flexible subscriptions: + +| Wildcard | Matches | Example | +| -------- | ------------------------------------ | ------------------------- | +| `*` | Single token (segment) | `*.my-workflow.*.*.*.*.*` | +| `>` | One or more tokens (rest of subject) | `*.*.*.*.*.token.>` | + +### Wildcard Examples + +``` +## All events for a specific workflow name (any execution) +*.my-workflow.*.*.*.*.> + +## All token events across all workflows +*.*.*.*.*.token.> + +## All workflow-level events (not activities) +*.*.*.*.*.*.workflow + +## All activity events +*.*.*.*.*.*.activity.> + +## Events for a specific execution +*.*.*.*.exec123.*.> + +## Events in a workflow tree (by root exec ID) +*.*.root123.*.*.*.> + +## Specific activity type across all workflows +*.*.*.*.*.*.activity.chat_activity.*.* +``` + +## API Routes + +### Stream Events Endpoint + +``` +GET /v1/workflows/executions/{execution_id}/stream +``` + +Query parameters for filtering: + +| Parameter | Description | Example | +| ------------------------ | ----------------------------------------- | --------------------------------- | +| `stream` | Filter by stream name | `?stream=token` | +| `workflow_name` | Filter by workflow name | `?workflow_name=my-workflow` | +| `workflow_exec_id` | Filter by execution ID | `?workflow_exec_id=exec789` | +| `root_workflow_exec_id` | Filter by root workflow exec ID | `?root_workflow_exec_id=root123` | +| `parent_workflow_exec_id`| Filter by parent workflow exec ID | `?parent_workflow_exec_id=parent456` | +| `activity_name` | Filter by activity name | `?activity_name=chat_activity` | +| `activity_id` | Filter by activity execution ID | `?activity_id=activity123` | +| `scope` | `workflow`, `activity`, or `*` | `?scope=activity` | +| `start_seq` | Resume from sequence | `?start_seq=42` | +| `metadata_filters` | Filter by metadata (JSON object) | `?metadata_filters={"key":"v"}` | +| `workflow_event_types` | Filter by workflow event types | `?workflow_event_types=WORKFLOW_COMPLETED` | + +### Example: Token Streaming + +```bash +curl -N "http://localhost:8000/v1/workflows/executions/${EXEC_ID}/stream?stream=token" +``` + +Response (Server-Sent Events): + +``` +data: {"stream":"token","data":{"event_type":"CUSTOM_TASK_IN_PROGRESS",...},"broker_sequence":1,"workflow_context":{...}} + +data: {"stream":"token","data":{"event_type":"CUSTOM_TASK_IN_PROGRESS",...},"broker_sequence":2,"workflow_context":{...}} + +data: {"stream":"token","data":{"event_type":"WORKFLOW_EXECUTION_COMPLETED",...},"broker_sequence":3,"workflow_context":{...}} +``` + +### Example: All Events with Wildcard + +```bash +## All events for an execution +curl -N "http://localhost:8000/v1/workflows/executions/${EXEC_ID}/stream" + +## Only activity events +curl -N "http://localhost:8000/v1/workflows/executions/${EXEC_ID}/stream?scope=activity" + +## Only workflow-level events +curl -N "http://localhost:8000/v1/workflows/executions/${EXEC_ID}/stream?scope=workflow" +``` + +## Sub-Workflow Events + +When a workflow spawns child workflows, events maintain parent-child relationships: + +```python +## Parent workflow +@workflows.workflow.define(name="parent-workflow") +class ParentWorkflow: + @workflows.workflow.entrypoint + async def run(self, params): + # Child workflow events will have parent_workflow_exec_id set + result = await workflows.workflow.execute_workflow( + ChildWorkflow, + params + ) + return result +``` + +### Consuming Events from Workflow Trees + +```python +## Get events from parent AND all children +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=parent_execution_id, +) +async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None: + continue + if payload.workflow_context.parent_workflow_exec_id: + print(f"[child] {payload.data}") + else: + print(f"[parent] {payload.data}") +``` + +### Filtering by Root ID + +Using NATS wildcards to get all events in a workflow tree: + +``` +## All events in a workflow tree by root exec ID +*.*.{root_exec_id}.*.*.*.> +``` + +## Event Types + +The SDK uses typed event responses discriminated by `event_type`. The full list of event types: + +| Event Type | Description | +| ------------------------------------ | ------------------------------ | +| `WORKFLOW_EXECUTION_STARTED` | Workflow started | +| `WORKFLOW_EXECUTION_COMPLETED` | Workflow finished successfully | +| `WORKFLOW_EXECUTION_FAILED` | Workflow failed | +| `WORKFLOW_EXECUTION_CANCELED` | Workflow was canceled | +| `WORKFLOW_EXECUTION_CONTINUED_AS_NEW`| Workflow continued as new | +| `WORKFLOW_TASK_TIMED_OUT` | Workflow task timed out | +| `WORKFLOW_TASK_FAILED` | Workflow task failed | +| `CUSTOM_TASK_STARTED` | Custom task started | +| `CUSTOM_TASK_IN_PROGRESS` | Custom task progress update | +| `CUSTOM_TASK_COMPLETED` | Custom task completed | +| `CUSTOM_TASK_FAILED` | Custom task failed | +| `CUSTOM_TASK_TIMED_OUT` | Custom task timed out | +| `CUSTOM_TASK_CANCELED` | Custom task canceled | +| `ACTIVITY_TASK_STARTED` | Activity started | +| `ACTIVITY_TASK_COMPLETED` | Activity finished | +| `ACTIVITY_TASK_RETRYING` | Activity is retrying | +| `ACTIVITY_TASK_FAILED` | Activity failed | + +Each event type maps to a specific Pydantic model (e.g. `WorkflowExecutionCompletedResponse`, +`CustomTaskInProgressResponse`) with fields like `event_id`, `event_timestamp`, `workflow_exec_id`, +`workflow_name`, and type-specific `attributes`. + +### Example Event + +```json +{ + "stream": "token", + "broker_sequence": 1, + "timestamp": "2025-01-15T10:30:00Z", + "data": { + "event_type": "WORKFLOW_EXECUTION_STARTED", + "event_id": "evt_abc123", + "event_timestamp": 1736938200000000000, + "workflow_name": "my-workflow", + "workflow_exec_id": "abc123", + "root_workflow_exec_id": "abc123", + "parent_workflow_exec_id": null, + "workflow_run_id": "run_456", + "attributes": {} + }, + "workflow_context": { + "workflow_name": "my-workflow", + "workflow_exec_id": "abc123", + "parent_workflow_exec_id": null + } +} +``` + +## Best Practices + +### 1. Always Handle Disconnection + +```python +async def resilient_consume(client, exec_id): + max_retries = 10 + + for attempt in range(max_retries): + try: + event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=exec_id, + ) + async with event_stream: + async for sse_event in event_stream: + payload = sse_event.data + if payload is None: + continue + yield payload + + if is_terminal_event(payload.data): + return + except ConnectionError: + await asyncio.sleep(min(2 ** attempt, 30)) +``` + +### 2. Filter Early + +Filter at the subscription level, not in your code: + +```python +## Good: Filter at source +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=exec_id, + stream="token", +) +async with event_stream: + async for sse_event in event_stream: + if sse_event.data is not None: + process(sse_event.data) + +## Bad: Filter in code (wastes bandwidth) +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=exec_id, +) +async with event_stream: + async for sse_event in event_stream: + if sse_event.data and sse_event.data.stream == "token": + process(sse_event.data) +``` + +### 3. Use Appropriate Scope + +If you only care about activity events: + +```python +event_stream = await client.workflows.events.get_stream_events_async( + workflow_exec_id=exec_id, scope="activity" +) +async with event_stream: + async for sse_event in event_stream: + if sse_event.data is not None: + # Only activity events, no workflow-level events + process(sse_event.data) +``` + +## Next Steps + +- [Publishing Events](streaming) - How to publish from workflows/activities +- [Streaming Architecture](../appendices/streaming) - Technical implementation details diff --git a/.claude/skills/workflows/references/guides/streaming.mdx b/.claude/skills/workflows/references/guides/streaming.mdx new file mode 100644 index 000000000..cd30339f2 --- /dev/null +++ b/.claude/skills/workflows/references/guides/streaming.mdx @@ -0,0 +1,148 @@ +--- +id: streaming +title: Streaming Events +sidebar_position: 7 +--- + +# Streaming: Real-Time Events from Workflows + +Stream events in real-time from your workflows and activities to power live UIs, progress indicators, and token-by-token LLM responses. + +## Quick Start + +### 1. Enable NATS + +```bash +NATS_ENABLED=true +NATS_HOST="nats://localhost:4222" +``` + +### 2. Publish from an Activity + +```python +import mistralai.workflows as workflows +from mistralai.workflows.core.task import Task + +@workflows.activity() +async def chat_activity(messages: list) -> dict: + initial_state = {"tokens": []} + + async with Task(type="token-stream", state=initial_state) as task: + + async for chunk in llm.stream(messages): + token = chunk.choices[0].delta.content + if token: + await task.update_state({"tokens": task.state["tokens"] + [token]}) + + return {"response": "".join(task.state["tokens"])} +``` + +### 3. Consume Events + +See [Consuming Streaming Events](streaming-consumption) for details on subscribing to events. + +## Publishing Patterns + +### Token Streaming (LLM) + +The most common pattern - stream tokens as they're generated: + +```python +@workflows.activity() +async def chat_activity(messages: list) -> dict: + initial_state = {"tokens": []} + + async with Task(type="token-stream", state=initial_state) as task: + + async for chunk in llm.stream(messages): + token = chunk.choices[0].delta.content + if token: + await task.update_state({"tokens": task.state["tokens"] + [token]}) + + return {"response": "".join(task.state["tokens"])} +``` + +### Progress Updates + +Report progress during long operations: + +```python +@workflows.activity(display_name="Processing text with explicit control") +async def streaming_tokens_with_progress_activity(text: str) -> dict: + """ + Example of explicit progress tracking with Task API. + + This pattern gives you full control over state updates and progress tracking. + """ + words = text.split() + initial_state = {"processed_words": [], "progress_idx": 0, "progress_total": len(words)} + + async with Task(type="progress-stream", state=initial_state) as task: + state = task.state + for i, word in enumerate(words): + await task.update_state({ + "processed_words": state["processed_words"] + [word], + "progress_idx": i + 1, + }) + state = task.state + await asyncio.sleep(0.1) + + final_state = task.state + return {"processed_text": " ".join(final_state["processed_words"]), "token_count": len(words)} +``` + +## Task Type Names + +The `type` parameter is a topic name you define. Best practices: + +| Good | Bad | +| --------------- | ---------- | +| `token` | `data` | +| `progress` | `Stream1` | +| `search_result` | `myStream` | + +- Use lowercase with underscores +- Keep names short and descriptive +- Use consistent naming across your app + +## Payload Limits + +NATS has a **1MB message limit**. For large data: + +```python +## Bad: Don't stream large payloads +## await task.update_state(large_document) + +## Good: Store large data externally, stream a reference +url = await storage.upload(large_document) +await task.update_state({"url": url, "size": len(large_document)}) +``` + +## Event Schema + +Each published event is wrapped with context: + +```python +{ + "stream": "token", # Your stream name + "sequence": 42, # Monotonic sequence number + "timestamp_unix_nano": ..., + "data": "Hello", # Your payload + "workflow_context": { + "namespace": "...", + "workflow_name": "my-workflow", + "workflow_exec_id": "abc123", + "parent_workflow_exec_id": null + }, + "activity_context": { # Only for activity events + "activity_name": "chat_activity", + "activity_exec_id": "1", + "activity_attempt_number": 1 + } +} +``` + +## Next Steps + +- [Consuming Streaming Events](streaming-consumption) - Subscribe and filter events +- [Streaming Architecture](../appendices/streaming) - Technical implementation details diff --git a/.claude/skills/workflows/references/guides/testing.md b/.claude/skills/workflows/references/guides/testing.md new file mode 100644 index 000000000..364fc5a92 --- /dev/null +++ b/.claude/skills/workflows/references/guides/testing.md @@ -0,0 +1,144 @@ +# Testing Workflows + +## Quick-test script + +The fastest way to verify a workflow works end-to-end. No test files, no conftest, no pytest: + +```bash +python .agents/skills/workflows/scripts/test_workflow.py src/workflows/my_workflow.py \ + --input '{"key": "value"}' \ + --timeout 30 +``` + +For interactive workflows that use `wait_for_input()`, provide `--interactions`: + +```bash +python .agents/skills/workflows/scripts/test_workflow.py src/workflows/my_workflow.py \ + --input '{}' \ + --interactions '[{"choice": "WFL"}]' \ + --timeout 60 +``` + +The script: +1. Discovers the workflow class in the given file +2. Starts a real worker in-process via `run_worker(detach=True)` — worker logs stream to stderr +3. Waits for the workflow to be registered on the Workflows API +4. Executes the workflow via `WorkflowsClient` +5. For interactive workflows: polls `__get_pending_inputs` and submits each `--interactions` entry in order +6. Waits for completion, prints PASSED + result JSON, or FAILED + traceback +7. On timeout: terminates the execution via the API and shuts down the worker +8. Exit code 0/1 for CI integration + +Options: +- `--input` (required): JSON input for the workflow +- `--timeout` (default 30): max seconds before the workflow is killed +- `--workflow-name`: select a specific workflow if the file contains multiple +- `--interactions`: JSON array of responses for interactive workflows + +Because the script uses a real worker, you get full worker logs (activity errors, retries, HTTP failures) in real time on stderr. No time-skipping — activities execute with real network calls. + +## Writing pytest tests + +For more control (multiple assertions, signal/query testing, spot-checks against external APIs), write pytest tests using `create_test_worker`. + +### Setup + +**conftest.py:** +```python +from mistralai.workflows.testing.fixtures import ( + clear_dependency_cache, # noqa: F401 + event_loop, # noqa: F401 + mock_upsert_search_attributes, # noqa: F401 + setup_test_config, # noqa: F401 + temporal_env, # noqa: F401 +) +``` + +**pyproject.toml:** +```toml +[tool.pytest.ini_options] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "session" + +[dependency-groups] +dev = ["pytest-asyncio>=0.25.3"] +``` + +### Basic test pattern + +```python +from datetime import timedelta +import asyncio +from mistralai.workflows.testing import create_test_worker + +# Keep timeouts aggressive for a tight feedback loop. +# Increase only when the workflow is known to be long-running. +WORKFLOW_EXECUTION_TIMEOUT = timedelta(seconds=10) + +async def test_my_workflow(self, temporal_env) -> None: + async with create_test_worker( + temporal_env, + workflows=[MyWorkflow], + activities=[my_activity], + ): + handle = await temporal_env.client.start_workflow( + "my-workflow", + {"key": "value"}, + id="test-my-workflow", + task_queue="test-task-queue", + execution_timeout=WORKFLOW_EXECUTION_TIMEOUT, + ) + result = await asyncio.wait_for(handle.result(), timeout=15) + assert result["key"] == "expected" +``` + +## Preventing hangs + +Temporal retries failed workflow tasks indefinitely by default. If your workflow has a bug that crashes during task processing (not activity execution), the test will hang forever. + +Two guards prevent this: + +1. **`execution_timeout`** on `start_workflow()` — Temporal kills the workflow after this duration. The time-skipping test server fast-forwards retry delays, so the timeout triggers in ~1 real second. + +2. **`asyncio.wait_for(handle.result(), timeout=N)`** — client-side fallback in case the execution timeout doesn't propagate cleanly. + +Always use both. + +## Common pitfalls + +### `start_to_close_timeout` must be a `timedelta` + +```python +# WRONG — causes infinite retry loop +@activity(start_to_close_timeout=60) + +# RIGHT +@activity(start_to_close_timeout=timedelta(seconds=60)) +``` + +### Sandbox blocks third-party imports (v3+) + +Determinism enforcement is enabled by default in v3. If your workflow file imports a library like `httpx` at the top level, the sandbox will block it. + +Fix: wrap non-deterministic imports with `workflow.unsafe.imports_passed_through()`: + +```python +from mistralai.workflows import workflow + +with workflow.unsafe.imports_passed_through(): + import httpx +``` + +Or disable enforcement per-workflow: `@workflow.define("name", enforce_determinism=False)` + +### Search attribute errors in test env + +The in-memory test environment doesn't have custom search attributes (e.g. `OtelTraceId`). The `mock_upsert_search_attributes` fixture from `mistralai.workflows.testing.fixtures` handles this. Make sure your conftest imports it. + +### Results are dicts, not Pydantic models + +Temporal returns raw dicts. Access fields directly: `result["message"]`, not `result.message`. + +### Don't add `_emit_*` activities manually + +`create_test_worker` already registers all workflow-lifecycle event activities. Adding them again causes duplicate-activity registration errors. diff --git a/.claude/skills/workflows/references/guides/workflows-exception.mdx b/.claude/skills/workflows/references/guides/workflows-exception.mdx new file mode 100644 index 000000000..7c51e69a2 --- /dev/null +++ b/.claude/skills/workflows/references/guides/workflows-exception.mdx @@ -0,0 +1,176 @@ +--- +id: workflows-exception +title: Workflows Exception +sidebar_position: 2 +--- + +# WorkflowsException: Structured Error Handling + +Workflows provides a structured exception system through `WorkflowsException` for consistent error handling across workflows and activities. + +## Overview + +`WorkflowsException` is the standard exception class for handling errors in workflows. It provides: + +- Structured error codes via `ErrorCode` enum +- HTTP status code mapping +- Serialization to JSON responses +- Factory methods for common error scenarios + +## Basic Usage + +```python +from mistralai.workflows.exceptions import WorkflowsException, ErrorCode +from http import HTTPStatus + +## Raise a custom exception +raise WorkflowsException( + code=ErrorCode.WORKFLOW_DEFINITION_ERROR, + message=f"{workflow} class must be decorated with @workflows.workflow.define", + status=http.HTTPStatus.INTERNAL_SERVER_ERROR +) +``` + +## Constructor Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `message` | `str` | required | Human-readable error message | +| `status` | `HTTPStatus` | `INTERNAL_SERVER_ERROR` | HTTP status code | +| `code` | `ErrorCode` | `TEMPORAL_SERVICE_ERROR` | Structured error code | +| `type` | `str` | `"invalid_request_error"` | Error type string | +## Error Codes + +The `ErrorCode` enum provides structured error codes organized by category: + +### General Errors (40**) + +| Code | Description | +|------|-------------| +| `execution_error` | General execution error | +| `temporal_error` | Temporal-related error | +| `temporal_service_error` | Temporal service error | +| `temporal_connection_error` | Connection to Temporal failed | +| `temporal_client_creation_error` | Failed to create Temporal client | +| `search_attributes_creation_error` | Failed to create search attributes | + +### Activity Errors (41**) + +| Code | Description | +|------|-------------| +| `activity_definition_error` | Activity incorrectly defined | +| `activity_not_found_error` | Activity not found | +| `invalid_arguments_error` | Invalid arguments provided | +| `activity_not_module_level` | Activity must be at module level | +| `tool_argument_error` | Tool argument error | +| `rate_limit_error` | Rate limit exceeded | + +### Workflow Errors (42**) + +| Code | Description | +|------|-------------| +| `workflow_definition_error` | Workflow incorrectly defined | +| `workflow_description_error` | Workflow description error | +| `workflow_already_started` | Workflow already running | +| `workflow_not_found` | Workflow not found | +| `invalid_params_error` | Invalid parameters | +| `workflow_timeout_error` | Workflow timed out | +| `workflow_signal_definition_error` | Signal incorrectly defined | +| `workflow_update_definition_error` | Update incorrectly defined | +| `workflow_query_error` | Fail to query a workflow | + +### Worker Errors (43**) + +| Code | Description | +|------|-------------| +| `worker_registration_error` | Worker registration failed | +| `worker_runtime_config_error` | Worker runtime configuration error | + +### Durable Agent Errors (44**) + +| Code | Description | +|------|-------------| +| `agent_execution_error` | Agent Execution Error | + +### Infrastructure Errors (45**) + +| Code | Description | +|------|-------------| +| `namespace_already_used` | Namespace already in use | +| `in_memory_cache_error` | In-memory cache error | +| `rejected_query_error` | Query was rejected | +| `unserializable_payload_error` | Payload cannot be serialized | +| `blob_storage_config_error` | Blob storage configuration error | + +### API Endpoint Errors (46**) + +The following error codes are used to identify specific errors that can occur when interacting with the workflow API. These codes follow the `{HTTP_METHOD}_{ENDPOINT}_ERROR` naming convention and are returned in the `code` field of error responses. + +| Code | Description | +|------|--------------------------------------------------| +| `get_workflows_error` | Error when retrieving workflows | +| `get_workflows_versions_error` | Error when retrieving workflow versions | +| `get_workers_whoami_error` | Error when retrieving worker identity | +| `post_workflows_register_error` | Error when registering a workflow | +| `post_workflows_execute_error` | Error when executing a workflow | +| `post_executions_terminate_error` | Error when terminating an execution | +| `get_executions_error` | Error when retrieving executions | +| `get_executions_trace_otel_error` | Error when retrieving execution OTEL trace | +| `get_executions_trace_summary_error` | Error when retrieving execution trace summary | +| `get_executions_trace_events_error` | Error when retrieving execution trace events | +| `post_executions_signals_error` | Error when sending a signal to an execution | +| `post_executions_queries_error` | Error when querying an execution | +| `post_executions_updates_error` | Error when updating an execution | +| `post_schedules_error` | Error when creating a schedule | +| `delete_schedules_error` | Error when deleting a schedule | +| `get_schedules_error` | Error when retrieving schedules | +| `post_executions_stream_error` | Error when streaming execution results | +| `get_events_stream_error` | Error when streaming events | +| `post_events_error` | Error when posting events | + +## Exception Properties and Methods + +### `from_temporal_error()` + +Create an exception from Temporal errors: + +```python +from temporalio.exceptions import TemporalError + +try: + await client.workflows.execute_workflow_async(...) +except TemporalError as e: + raise WorkflowsException.from_temporal_error( + temporal_error=e, + message_override="Failed to execute workflow", + status_override=HTTPStatus.SERVICE_UNAVAILABLE + ) +``` + +Handles specific Temporal error types: +- `RPCError` → Maps to appropriate error code based on RPC status +- `WorkflowQueryRejectedError` → `REJECTED_QUERY_ERROR` +- `WorkflowAlreadyStartedError` → `WORKFLOW_ALREADY_STARTED` + +### `from_api_client_error()` + +Create an exception from HTTP client errors: + +```python +import httpx + +try: + response = await client.get("https://api.example.com") +except httpx.HTTPError as e: + raise WorkflowsException.from_api_client_error( + exc=e, + message="External API call failed", + code=ErrorCode.EXECUTION_ERROR, + ) +``` + +## Next Steps + +- [Workflows Guide](workflows) - Learn about workflow fundamentals +- [Activities Guide](activities) - Learn how to implement activities +- [Observability Guide](observability) - Monitor and trace your workflows diff --git a/.claude/skills/workflows/references/guides/workflows-plugins.mdx b/.claude/skills/workflows/references/guides/workflows-plugins.mdx new file mode 100644 index 000000000..d4468811a --- /dev/null +++ b/.claude/skills/workflows/references/guides/workflows-plugins.mdx @@ -0,0 +1,201 @@ +--- +id: workflows-plugins +title: Workflows Plugins +sidebar_position: 8 +--- + +# Workflows Plugins + +Mistral Workflows plugins provide reusable components that accelerate workflow development. They are standard Python packages that expose workflows, activities, and dependencies under the unified `mistralai.workflows.plugins` namespace. + +## Overview + +Plugins are development-time enablers that let you import curated, tested building blocks rather than implementing common patterns from scratch. They work like any Python package—install with `pip` and import into your code. + +```python +from mistralai.workflows.plugins.mistralai import mistralai_chat_complete +from mistralai.workflows.plugins.mistralai import Agent, Runner +``` + +## Official Plugins + +### Mistral AI Plugin + +**Package:** `mistralai-workflows-plugins-mistralai` + +Provides native Mistral AI integration for LLM operations, agent execution, session management, and MCP support within workflows. + +**Key activities:** + +| Activity | Description | +|----------|-------------| +| `mistralai_chat_complete` | Single-turn chat completion | +| `mistralai_chat_stream` | Streaming chat completion | +| `mistralai_chat_parse` | Chat completion with structured output parsing | +| `mistralai_embeddings` | Generate text embeddings | +| `mistralai_ocr` | Extract text from images and documents | +| `mistralai_create_agent` | Create a remote agent | +| `mistralai_update_agent` | Update a remote agent | +| `mistralai_start_conversation` | Start an agent conversation | +| `mistralai_start_conversation_stream` | Start an agent conversation with streaming | +| `mistralai_append_conversation` | Add messages to an existing conversation | +| `mistralai_append_conversation_stream` | Add messages to an existing conversation with streaming | + +**Key components:** + +| Component | Description | +|-----------|-------------| +| `Agent` | Agent definition with model, tools, and configuration | +| `Runner` | Orchestrates agent execution with conversation and tool loops | +| `LocalSession` | Local in-process agent session | +| `RemoteSession` | Remote stateful agent session | +| `MCPStdioConfig` | Configuration for local MCP servers (stdio) | +| `MCPSSEConfig` | Configuration for remote MCP servers (SSE) | +| `MCPConfig` | Union type alias for `MCPStdioConfig \| MCPSSEConfig` | +| `collect_mcp_tools` | Activity: collect tool definitions from one or more MCP servers | +| `execute_mcp_tool` | Activity: execute a named tool on its MCP server | +| `get_mistral_client` | Dependency: returns a configured `mistralai.Mistral` client | + +**Example — chat completion:** + +```python +from mistralai.workflows import workflow +from mistralai.workflows.plugins.mistralai import ( + ChatCompletionRequest, + UserMessage, + mistralai_chat_complete, +) + +@workflow() +async def summarize(text: str) -> str: + request = ChatCompletionRequest( + model="mistral-large-latest", + messages=[UserMessage(content=f"Summarize this text:\n\n{text}")], + ) + result = await mistralai_chat_complete(request) + return result.choices[0].message.content +``` + +For full documentation on `Agent`, `Runner`, sessions, MCP, and multi-agent handoffs, see the [Durable Agents guide](./durable-agents). + +### Webhook Plugin + +**Package:** `mistralai-workflows-plugins-webhook` + +Provides HTTP/webhook routing and fan-out for building event-driven workflows. Use it to receive webhooks from external services (GitHub, Slack, Linear, etc.) and route them to running workflow instances. + +**Key components:** + +| Component | Description | +|-----------|-------------| +| `HTTPRouterWorkflow` | Base class for webhook router workflows | +| `RouteDispatcher` | Dispatches incoming HTTP requests to handler methods | +| `SearchableWorkflow` | Mixin to expose search attributes for workflow targeting | +| `post` | Decorator to register a method as an HTTP POST handler | +| `SearchAttribute` | Key/value pair used to target running workflow instances | +| `search_workflow_execution_ids` | Find running workflow instance IDs matching search attributes | +| `send_signal` | Fan out a signal to multiple workflow instances | +| `WebhookFanOutResult` | Result of `send_signal` (list of target IDs and dispatched count) | +| `HTTPRequest` / `HTTPResponse` | Request and response types | + +Provider-specific helpers (experimental) are available under `webhook.github`, `webhook.slack`, and `webhook.linear`. + +**Example:** + +```python +from mistralai.workflows import workflow +from mistralai.workflows.plugins.webhook import ( + HTTPRouterWorkflow, + HTTPRequest, + HTTPResponse, + post, +) + +@workflow.define(name="my-webhook-router") +class MyWebhookRouter(HTTPRouterWorkflow): + @post("/") + async def handle(self, request: HTTPRequest) -> HTTPResponse: + # process the webhook payload + return HTTPResponse(status_code=200, body={"ok": True}) +``` + +### Nuage Plugin + +**Package:** `mistralai-workflows-plugins-nuage` + +Orchestrates AI coding agents inside sandboxed containers. Powers Le Chat's Vibe coding feature — running an agentic loop where an LLM reasons, invokes tools, and executes code in isolation. + +Key capabilities: sandboxed code execution (Demiurge), GitHub integration, and Le Chat streaming. + +See the [Nuage Plugin guide](./nuage-plugin) for full documentation, installation instructions, and configuration reference. + +## Installation + +Install the plugins you need alongside the main SDK: + +```bash +pip install mistralai-workflows +pip install mistralai-workflows-plugins-mistralai +pip install mistralai-workflows-plugins-webhook +``` + +See the [Nuage Plugin guide](./nuage-plugin) for installation instructions. + +## Creating Your Own Reusable Libraries + +You can create custom packages to share reusable workflows, activities, and dependencies within your organization. + +:::note +The `mistralai.workflows.plugins` namespace is reserved for Mistral-supported plugins. For your own reusable code, use your own top-level package name. +::: + +### Directory Structure + +``` +acme-workflows/ +├── pyproject.toml +├── acme_workflows/ +│ ├── __init__.py +│ ├── activities.py # Your reusable activities +│ └── workflows.py # Your reusable workflows +└── tests/ +``` + +### Build Configuration + +In your `pyproject.toml`: + +```toml +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "acme-workflows" +version = "0.1.0" +dependencies = [ + "mistralai-workflows>=2.0.0", +] +``` + +### Usage + +After installing your package, import directly: + +```python +from acme_workflows.activities import my_custom_activity +from acme_workflows.workflows import my_rag_pipeline +``` + +## Contributing Plugins to Mistral + +If you have built a reusable plugin that you believe would benefit the broader community, you can contribute it to the official `mistralai.workflows.plugins` namespace. + +To contribute: + +1. **Open an issue first** — Before investing time in a pull request, file an issue describing your plugin and its use case. This helps determine whether Mistral would be willing to accept and maintain your contribution. + +2. **Open a pull request** — If the issue is positively received, submit a PR to the Mistral workflows repository following the plugin structure documented in `workflow_sdk/plugins/CONTRIBUTING.md`. + +Contributed plugins become part of the official Mistral-supported plugin ecosystem and will be maintained alongside other official plugins. + diff --git a/.claude/skills/workflows/references/guides/workflows.mdx b/.claude/skills/workflows/references/guides/workflows.mdx new file mode 100644 index 000000000..bf3582bbd --- /dev/null +++ b/.claude/skills/workflows/references/guides/workflows.mdx @@ -0,0 +1,626 @@ +--- +id: workflows +title: Workflows +sidebar_position: 1 +--- + +# Workflows: The Brains of Your Application + +Workflows define the high-level business logic and coordinate activities in your application. + +## What is a Workflow? + +A workflow is a durable, deterministic process that orchestrates activities and manages execution state. Key characteristics: + +- Defined as Python classes +- Can run for seconds to years (with proper checkpointing) +- Maintains complete execution history +- Automatically recovers from failures +- Input/output limited to 2MB +- Timeout if synchronous CPU-bound Python code exceeds 2 seconds ([see more](./limitations)) + +## Workflow Determinism + +Workflows must be **deterministic**: given the same inputs, they must always produce the same +sequence of commands. This is a fundamental requirement of Temporal's replay mechanism: +when a worker restarts or a workflow is recovered, Temporal re-executes the workflow code +from the beginning, matching each command against the recorded event history. +If the code produces different commands on replay, the workflow fails with a non-determinism error. + +:::note +Determinism is **enforced by default**. Workflow code runs inside Temporal's Python sandbox, which intercepts non-deterministic calls and raises errors at runtime. You can opt out per-workflow with `enforce_determinism=False` or worker-wide with `DEFAULT_ENFORCE_DETERMINISM=0`. See [Determinism Enforcement](#determinism-enforcement-sandbox) below for details. +::: + +### Correct: Use Temporal's Deterministic APIs + +Always use these in workflow code: + +```python +from temporalio import workflow as temporal_wf + +## ✅ Correct - Use workflow.now() for current time +current_time = temporal_wf.now() + +## ✅ Correct - Use workflow.uuid4() for UUIDs +request_id = temporal_wf.uuid4() + +## ✅ Correct - Use workflow.random() for random values +random_value = temporal_wf.random() +``` + +### Dangerous: Standard Library Equivalents + +Don't use these directly in workflow code: + +```python +## ❌ Dangerous - use temporalio.workflow.now() instead +from datetime import datetime +current_time = datetime.now() + +## ❌ Dangerous - use temporalio.workflow.uuid4() instead +import uuid +request_id = uuid.uuid4() + +## ❌ Dangerous - use temporalio.workflow.random() instead +import random +rand_val = random.random() +``` + +Also dangerous in workflows: +- File system access (`open()`, `os.listdir()`, etc.) +- Direct HTTP calls or database queries +- Modifying global variables +- System calls (`os.environ`, `os.getcwd()`, etc.) + +### Move Non-Deterministic Work to Activities + +For operations like external API calls, database queries, or file I/O, use activities: + +```python +from mistralai.workflows import workflow +from temporalio import workflow as temporal_wf + +@workflow.activity +async def fetch_external_data(params: DataParams) -> ExternalData: + # ✅ Safe - Activities are not replayed + response = await http_client.get(params.url) + timestamp = datetime.now() # OK in activities + return ExternalData(data=response.json(), fetched_at=timestamp) + +@workflow.define(name="my_workflow") +class MyWorkflow: + @workflow.entrypoint + async def run(self, params: MyParams) -> MyResult: + # ✅ Correct - Non-deterministic work in activity + data = await temporal_wf.execute_activity( + fetch_external_data, + params, + start_to_close_timeout=timedelta(seconds=30), + ) + return MyResult(data=data) +``` + +### Determinism Enforcement (Sandbox) + +When determinism enforcement is enabled, your workflow code runs inside Temporal's Python sandbox. The sandbox: + +- **Re-imports modules** in an isolated environment so that side-effectful module-level code + is contained +- **Intercepts dangerous standard library calls** such as `datetime.now()`, `random.random()`, + `uuid.uuid4()`, `open()`, `os.environ`, and other non-deterministic operations +- **Restricts the asyncio event loop** to prevent spawning uncontrolled coroutines + +If your workflow code attempts any of these operations, the sandbox raises an error at runtime +rather than silently producing a non-determinism bug that surfaces only on replay. + +:::info +If you disable the sandbox, determinism is your responsibility. Your code will still break on replay if it's non-deterministic, you just won't get an immediate error telling you so. +You can find more details about Temporal's sandbox [here](https://docs.temporal.io/develop/python/python-sdk-sandbox). +::: + +When enabled, the SDK sets `sandboxed=True` on the underlying Temporal workflow definition. +No custom sandbox policy is applied, the standard Temporal sandbox behavior is used as-is. + +#### Disabling Per-Workflow + +Determinism enforcement is enabled by default. To opt out for a specific workflow, pass +`enforce_determinism=False` to the `@workflow.define` decorator: + +```python +from mistralai.workflows import workflow + +@workflow.define(name="my_workflow", enforce_determinism=False) +class MyWorkflow: + @workflow.entrypoint + async def run(self, input: str) -> str: + # This code runs WITHOUT the Temporal sandbox. + # You are responsible for ensuring determinism. + return f"Processed: {input}" +``` + +#### Disabling Worker-Wide + +Set the `DEFAULT_ENFORCE_DETERMINISM` environment variable to disable sandboxing for all +workflows on a worker by default: + +```bash +DEFAULT_ENFORCE_DETERMINISM=0 +``` + +This sets `config.worker.default_enforce_determinism` to `False`. + +#### Precedence + +The decorator-level setting takes priority over the environment variable: + +| `@workflow.define(enforce_determinism=...)` | `DEFAULT_ENFORCE_DETERMINISM` | Result | +|---|---|---| +| `True` | any | sandboxed | +| `False` | any | **not** sandboxed | +| not set (default) | `1` / `True` (default) | sandboxed | +| not set (default) | `0` / `False` | **not** sandboxed | + +#### Temporarily Bypassing the Sandbox + +:::warning +These escape hatches defeat the purpose of determinism enforcement. +Use them only when you understand the implications and have no alternative. +::: + +When determinism enforcement is enabled, you may occasionally need to perform an operation +that the sandbox blocks, for example importing a module that has non-deterministic side +effects at import time, or performing a one-off read that you know is safe. + +The SDK exposes two context managers under `workflow.unsafe`: + +**`workflow.unsafe.imports_passed_through()`** — Allows imports inside the context to bypass the sandbox's module re-import mechanism. +Use this when a third-party library performs side effects at import time that conflict +with the sandbox. + +```python +from mistralai.workflows import workflow + +@workflow.define(name="my_workflow") +class MyWorkflow: + @workflow.entrypoint + async def run(self, input: str) -> str: + with workflow.unsafe.imports_passed_through(): + import some_problematic_library + # Use the library normally after the import + return some_problematic_library.process(input) +``` + +**`workflow.unsafe.skip_determinism_enforcement()`** — Temporarily disables all sandbox restrictions within the context. +Code inside this block runs as if `enforce_determinism=False`. + +```python +from mistralai.workflows import workflow + +@workflow.define(name="my_workflow") +class MyWorkflow: + @workflow.entrypoint + async def run(self, input: str) -> str: + with workflow.unsafe.skip_determinism_enforcement(): + # Sandbox restrictions are lifted here. + # You are responsible for ensuring determinism. + import os + value = os.environ.get("SOME_CONFIG", "default") + return f"Processed: {input} with {value}" +``` + +Both context managers map directly to their Temporal equivalents +(`temporalio.workflow.unsafe.imports_passed_through()` and +`temporalio.workflow.unsafe.sandbox_unrestricted()` respectively). + +#### Recommendations + +1. **Keep enforcement enabled.** Determinism enforcement is on by default. Catching + non-determinism at development time is far cheaper than debugging replay failures + in production. Only disable it temporarily while migrating legacy workflows. +2. **Migrate existing workflows incrementally.** If you have workflows that are not yet + compliant, set `enforce_determinism=False` on those specific workflows while you fix + them. Avoid disabling enforcement worker-wide. +3. **Keep unsafe blocks small and documented.** When you must bypass the sandbox, wrap the + minimum amount of code and leave a comment explaining why. +4. **Move side effects to activities.** The best way to avoid sandbox issues is to keep + workflow code pure orchestration logic. All I/O, network calls, and non-deterministic + operations belong in activities. + +## Defining a Workflow + +Basic workflow structure: + +```python +import mistralai.workflows as workflows + +@workflows.workflow.define(name="report_workflow") +class MyWorkflow: + @workflows.workflow.entrypoint + async def run(self, report_type: str, include_details: bool = False) -> dict: + """Workflow implementation""" + # Orchestrate activities here + pass +``` + +:::warning +**All workflow entrypoint parameters and return types must have type annotations.** An entrypoint missing any type hint on its arguments or return value will fail validation at registration time. Always provide explicit types for every parameter and the return type. + +```python +## ❌ Invalid - missing type annotations +@workflows.workflow.define(name="my_workflow") +class BadWorkflow: + @workflows.workflow.entrypoint + async def run(self, data, count=1): + pass + +## ✅ Valid - all parameters and return type are annotated +@workflows.workflow.define(name="my_workflow") +class GoodWorkflow: + @workflows.workflow.entrypoint + async def run(self, data: str, count: int = 1) -> dict: + pass +``` +::: + +## Workflow Input + +The `run` method accepts any JSON-serializable types as parameters. + +### Primitive and multi-parameter inputs + +```python +@workflow.define(name="report_workflow") +class ReportWorkflow: + @workflow.entrypoint + async def run(self, report_type: str, include_details: bool = False) -> dict: + ... +``` + +Input: +```json +{"report_type": "daily", "include_details": false} +``` + +### Single Pydantic model + +When the entrypoint takes a single Pydantic `BaseModel`, its fields become the top-level input keys — there is no wrapper object: + +```python +from pydantic import BaseModel + +class ReportParams(BaseModel): + report_type: str + include_details: bool = False + +@workflow.define(name="report_workflow") +class ReportWorkflow: + @workflow.entrypoint + async def run(self, params: ReportParams) -> dict: + ... +``` + +Input: +```json +{"report_type": "daily", "include_details": true} +``` + +### Union of Pydantic models + +When a workflow can be triggered with different input shapes, use a union of `BaseModel` subclasses: + +```python +from pydantic import BaseModel, ConfigDict + +class PromptInput(BaseModel): + model_config = ConfigDict(extra="forbid") + prompt: str + +class CountInput(BaseModel): + model_config = ConfigDict(extra="forbid") + count: int + +@workflow.define(name="flexible_workflow") +class FlexibleWorkflow: + @workflow.entrypoint + async def run(self, params: PromptInput | CountInput) -> str: + if isinstance(params, PromptInput): + return f"prompt: {params.prompt}" + return f"count: {params.count}" +``` + +The SDK validates the input against each member in order and passes the first match to the handler: + +```json +{"prompt": "hello"} → PromptInput +{"count": 42} → CountInput +{"count": 42, "extra": "field"} → ValidationError +``` + +>Tip: Use `extra="forbid"` on each member model. It produces precise discrimination and clear error messages when the input doesn't match any expected shape. + +### Optional union (`| None`) + +Append `| None` (or use `Optional` from `typing`) to make the input optional: + +```python +@workflow.define(name="optional_workflow") +class OptionalWorkflow: + @workflow.entrypoint + async def run(self, params: PromptInput | CountInput | None) -> str: + if params is None: + return "no input" + if isinstance(params, PromptInput): + return f"prompt: {params.prompt}" + return f"count: {params.count}" +``` + +### Mixing BaseModel with primitive types + +Unions that combine a `BaseModel` with a non-model type (`str`, `int`, etc.) are not supported and will fail when the `@workflow.entrypoint` decorator is applied: + +```python +## ❌ Raises TypeError at class definition time +@workflow.define(name="my_workflow") +class MyWorkflow: + @workflow.entrypoint + async def run(self, params: PromptInput | str) -> str: + ... +``` + +``` +TypeError: Parameter 'params' of 'run' has unsupported union members: str. +Union inputs only support Pydantic BaseModel subclasses and None. +Use a dedicated BaseModel instead. +``` + +If you genuinely need a union that includes a non-model type, define a named `RootModel` subclass and use that instead: + +```python +from pydantic import BaseModel, RootModel + +class PromptInput(BaseModel): + prompt: str + +class PromptOrRaw(RootModel[PromptInput | str]): + pass + +@workflow.define(name="my_workflow") +class MyWorkflow: + @workflow.entrypoint + async def run(self, params: PromptOrRaw) -> str: + if isinstance(params.root, PromptInput): + return f"structured: {params.root.prompt}" + return f"raw: {params.root}" +``` + +The named subclass also produces a cleaner generated JSON schema. + +## Execution Timeout + +Every workflow has a maximum total execution time — `execution_timeout` — that caps its lifetime including all retries and [continue-as-new](#7-continue-as-new) iterations. + +Set it on the `@workflow.define` decorator: + +```python +from datetime import timedelta +from mistralai.workflows import workflow + +@workflow.define(name="my_workflow", execution_timeout=timedelta(hours=4)) +class MyWorkflow: + @workflow.entrypoint + async def run(self, params: MyParams) -> MyResult: + ... +``` + +When the workflow is started via the API, the platform uses this value as the hard cap. A workflow that is still running (or waiting) after `execution_timeout` has elapsed is cancelled with a `WORKFLOW_EXECUTION_TIMED_OUT` error. + +**Default: 1 hour.** Workflows that need to run longer must opt in explicitly: + +```python +@workflow.define(name="long_running_workflow", execution_timeout=timedelta(days=7)) +class LongRunningWorkflow: + ... +``` + +:::note +`execution_timeout` is a *total* wall-clock limit, not an activity timeout. Individual activity timeouts are controlled by `start_to_close_timeout` / `schedule_to_close_timeout` on each activity call. +::: + +## Core Workflow Features + +### 1. Signals + +Handle external events with signals: + +```python +@workflows.workflow.signal(name="approve", description="Approval signal") +async def approve_signal(self, data: ApprovalData): + self.approved = True +``` + +[Learn more about signals](./signals-queries-updates) + +### 2. Queries + +Expose workflow state through queries: + +```python +@workflows.workflow.query(name="get_status", description="Get current status") +def get_status(self) -> WorkflowStatus: + return WorkflowStatus( + progress=self.progress, + status=self.current_status + ) +``` + +[Learn more about queries](./signals-queries-updates) + +### 3. Updates + +Handle workflow updates: + +```python +@workflows.workflow.update(name="update_config", description="Update workflow configuration") +async def update_config(self, config: UpdateConfig) -> UpdateResult: + # Handle update + return UpdateResult(success=True) +``` + +[Learn more about updates](./signals-queries-updates) + +### 4. Schedules + +Define scheduled workflow executions using cron expressions: + +```python +from mistralai.workflows.models import ScheduleDefinition + +schedule = ScheduleDefinition( + input={"report_type": "daily", "include_details": False}, # Input parameters for scheduled runs + cron_expressions=["0 0 * * *"] # Run daily at midnight +) + +@workflows.workflow.define(name="report_workflow", schedules=[schedule]) +class ReportWorkflow: + async def run(self, report_type: str = "daily", include_details: bool = False): + # Generate report + pass +``` + +Key schedule features: + +- Cron-based scheduling with standard cron expressions +- Input parameters for scheduled executions +- Multiple cron expressions can be specified + +[Learn more about scheduling](./scheduling) + +### 5. Child Workflows + +Execute other workflows as children: + +```python +from datetime import timedelta +from mistralai.workflows import workflow + +result = await workflow.execute_workflow( + ChildWorkflow, + params=child_params, + execution_timeout=timedelta(hours=1) +) +``` + +You can also start a child workflow without waiting for its result (fire-and-forget) by passing `wait=False`: + +```python +handle = await workflow.execute_workflow( + ChildWorkflow, + params=child_params, + execution_timeout=timedelta(hours=1), + wait=False, +) +## Parent continues immediately — child runs independently +## Optionally await later: result = await handle +``` + +By default, `wait=False` sets the parent close policy to `ABANDON`, so the child continues running even if the parent completes. You can override this with the `parent_close_policy` parameter: + +```python +from mistralai.workflows import ParentClosePolicy + +handle = await workflow.execute_workflow( + ChildWorkflow, + params=child_params, + execution_timeout=timedelta(hours=1), + wait=False, + parent_close_policy=ParentClosePolicy.TERMINATE, +) +``` + +### 6. Waiting for Conditions + +Wait for conditions to be met: + +```python +from mistralai.workflows import workflow +await workflow.wait_condition( + lambda: self.ready, + timeout=timedelta(minutes=5) +) +``` + +### 7. Continue-As-New + +Reset workflow history for long-running or iterative workflows. Use this to prevent history from growing too large when processing large datasets or running indefinitely. + +**Important:** Your workflow's `run` method must accept a state parameter to restore its state when continuing as new. This parameter will receive the state passed to `continue_as_new()`. + +```python +import mistralai.workflows as workflows + +@workflows.workflow.define(name="long-running-processor") +class LongRunningProcessor: + @workflows.workflow.entrypoint + async def run(self, page: int = 0, total_processed: int = 0): # State parameters are required + # Process current page + + # Check if history is getting large + if workflows.workflow.should_continue_as_new(): + # Continue with fresh history + workflows.workflow.continue_as_new({"page": page + 1, "total_processed": total_processed + 100}) + + return await self.run(page + 1, total_processed + 100) +``` + +**When to use continue-as-new:** + +- Long-running workflows that iterate indefinitely +- When workflow history approaches size limits (~50K events) +- Workflows that need to run for weeks/months + +For complete working examples see: +[`workflow_with_continue_as_new.py`](https://github.com/mistralai/mistral-internal/blob/main/workflow_sdk/mistral_workflows/examples/workflow_with_continue_as_new.py) + +## Advanced Features + +### 1. Reset Mechanism + +You can "reset" a Workflow Execution to restart it from a specific point in its history. This is useful if your workflow gets stuck (for example, because of a non-deterministic error) or cannot complete. When you reset, the workflow ends its current run and starts again from the event you pick in the event history. + +All workflow events up to the reset point are copied to the new execution. The workflow then continues running from there using the latest version of your code. Any progress made after the reset event will be lost. + +Resetting should only be used after fixing the underlying problem. It's best practice to provide a reason for the reset, which is recorded in the workflow's event history. + +```python +await client.workflows.executions.reset_workflow_async( + execution_id="your-execution-id", + event_id=42, # Must be a WORKFLOW_TASK_COMPLETED event + reason="Bug fixed in activity logic", + exclude_signals=True, # Optional: skip replaying signals after this point + exclude_updates=True # Optional: skip replaying updates after this point +) +``` + +### 2. Workflow Execution Context + +Access workflow execution information: + +```python +from mistralai.workflows import workflow +execution_id = workflow.get_execution_id() +``` + +## Workflow vs Activity + +| Feature | Workflow | Activity | +| ----------- | -------------------- | ------------------ | +| Duration | Seconds to years | Seconds to minutes | +| State | Stateful | Stateless | +| Retries | Manual recovery | Automatic | +| Parallelism | Complex coordination | Single operation | + +## Next Steps + +- [Activity Guide](activities) - Learn how to implement work units +- [Observability Guide](observability) - Monitor and trace your workflows +- [Scheduling Guide](scheduling) - Learn about advanced scheduling options diff --git a/.claude/skills/workflows/references/pipeline_pattern.md b/.claude/skills/workflows/references/pipeline_pattern.md new file mode 100644 index 000000000..338bbf5f9 --- /dev/null +++ b/.claude/skills/workflows/references/pipeline_pattern.md @@ -0,0 +1,236 @@ +# Pipeline/Sequential Steps Pattern + +Build multi-step workflows with declarative step definitions, conditional execution, and configurable error handling. + +## Overview + +The pipeline pattern provides a structured way to: +- Define ordered sequences of workflow steps +- Execute steps as child workflows with deterministic IDs +- Handle errors per-step (raise, skip, or stop) +- Conditionally skip steps based on runtime context + +This pattern can be very useful to define a core workflow that can be extended by different variants. + +## Core Components + +### StepSpec + +Declarative definition of a pipeline step: + +```python +from dataclasses import dataclass +from enum import Enum +from typing import Any, Callable, Optional, Type +from pydantic import BaseModel + +# Error handling policies +class OnError(Enum): + RAISE = "raise" # Re-raise exception (default) + SKIP = "skip" # Log warning and continue to next step + STOP = "stop" # Log warning and stop pipeline (no error raised) + +# Context type for passing data between steps +Context = dict[str, Any] + +# Factory function signature for creating step parameters +ParamsFactory = Callable[[Any, Context, str, str], BaseModel] + +@dataclass(frozen=True) +class StepSpec: + """Declarative definition of a step in the pipeline.""" + task: "RegisteredTask" + when: Optional[Callable[[Context, Any], bool]] = None # Conditional execution + on_error: OnError = OnError.RAISE +``` + +### RegisteredTask + +Links a task type to its workflow class and parameter factory: + +```python +from enum import Enum + +class TaskRegistry(Enum): + """Registry of all available task types.""" + VALIDATE_INPUT = "VALIDATE_INPUT" + PROCESS_DATA = "PROCESS_DATA" + GENERATE_OUTPUT = "GENERATE_OUTPUT" + SEND_NOTIFICATION = "SEND_NOTIFICATION" + +@dataclass(frozen=True) +class RegisteredTask: + task_type: TaskRegistry + workflow_cls: Type # Abraxas workflow class + make_params: ParamsFactory # Creates Pydantic params from context +``` + +### Task Registry + +Central mapping of task types to their implementations: + +```python +BASE_TASKS = { + TaskRegistry.VALIDATE_INPUT: RegisteredTask( + task_type=TaskRegistry.VALIDATE_INPUT, + workflow_cls=ValidateInputTask, + make_params=lambda data, ctx, exec_id, process_type: ValidateParams( + data=data, + rules=ctx["validation_rules"], + ), + ), + TaskRegistry.PROCESS_DATA: RegisteredTask( + task_type=TaskRegistry.PROCESS_DATA, + workflow_cls=ProcessDataTask, + make_params=lambda data, ctx, exec_id, process_type: ProcessParams( + data=data, + config=ctx["processing_config"], + ), + ), +} +``` + +## Pipeline Implementation + +### Base Pipeline Class + +```python +import logging +from typing import Any, ClassVar, Generic, TypeVar + +import mistralai.workflows as workflows + +from .process_utils import Context, OnError, RegisteredTask, StepSpec +from .utils import get_child_workflow_execution_id + +logger = logging.getLogger(__name__) +T = TypeVar("T") + +class CoreProcess(Generic[T]): + """Base class for declarative pipelines.""" + + STEPS: ClassVar[list[StepSpec]] = [] + PROCESS_TYPE: str = "default" + + def __init__(self) -> None: + self.steps = self.STEPS + self.process_type = self.PROCESS_TYPE + + async def run_pipeline(self, input_object: T, ctx: Context, case_id: str) -> T: + """Execute all steps in sequence.""" + for step in self.steps: + step_label = step.task.task_type.name.lower() + + # Conditional execution + if step.when and not step.when(ctx, input_object): + logger.info(f"Skipping step {step_label}") + continue + + reg: RegisteredTask = step.task + execution_id = get_child_workflow_execution_id(task_name=step_label, case_id=case_id) + + logger.info(f"Running step {step_label} ...") + + try: + params = reg.make_params(input_object, ctx, execution_id, self.process_type) + input_object = await workflows.workflow.execute_workflow( + reg.workflow_cls, params, execution_id=execution_id + ) + logger.info(f"Step {step_label} OK") + + except Exception as exc: + logger.exception(f"Step {step_label} failed: {exc}") + + if step.on_error is OnError.RAISE: + raise + if step.on_error is OnError.STOP: + logger.warning(f"Stopping pipeline after '{step_label}'") + break + if step.on_error is OnError.SKIP: + logger.warning(f"Skipping failed step '{step_label}'") + continue + + return input_object +``` + +### Concrete Pipeline Definition + +```python +from typing import ClassVar + +class DataProcessingPipeline(CoreProcess[ProcessedData]): + """Pipeline for processing incoming data.""" + + STEPS: ClassVar[list[StepSpec]] = [ + StepSpec(task=BASE_TASKS[TaskRegistry.VALIDATE_INPUT]), + StepSpec(task=BASE_TASKS[TaskRegistry.PROCESS_DATA], on_error=OnError.SKIP), + StepSpec( + task=BASE_TASKS[TaskRegistry.SEND_NOTIFICATION], + when=lambda ctx, data: ctx.get("notify_on_complete", False), + ), + ] + + PROCESS_TYPE = "data_processing" + + def __init__(self, initial_data: InputData, config: Config) -> None: + super().__init__() + self.initial_data = initial_data + self.config = config + + async def run(self) -> ProcessedData: + ctx = { + "validation_rules": self.config.rules, + "processing_config": self.config.processing, + "notify_on_complete": self.config.notifications_enabled, + } + return await self.run_pipeline( + input_object=self.initial_data, ctx=ctx, case_id=self.initial_data.id + ) +``` + +## Usage Patterns + +### Conditional Step Execution + +```python +StepSpec( + task=BASE_TASKS[TaskRegistry.SEND_NOTIFICATION], + when=lambda ctx, data: data.requires_notification and ctx.get("notifications_enabled"), +) +``` + +### Error Handling Strategies + +```python +# Fail fast (default) - pipeline stops on first error +StepSpec(task=..., on_error=OnError.RAISE) + +# Skip failed steps - continue with remaining steps +StepSpec(task=..., on_error=OnError.SKIP) + +# Stop gracefully - no error raised, but pipeline ends +StepSpec(task=..., on_error=OnError.STOP) +``` + +### Extending Pipelines for Variants + +```python +class FrenchDataPipeline(DataProcessingPipeline): + """French-specific pipeline with additional steps.""" + + STEPS: ClassVar[list[StepSpec]] = [ + *DataProcessingPipeline.STEPS, + StepSpec(task=FRENCH_TASKS[TaskRegistry.FRENCH_VALIDATION]), + ] + + PROCESS_TYPE = "french_data_processing" +``` + +## Best Practices + +1. **Keep steps focused**: Each step should do one thing well +2. **Use context for shared data**: Pass data between steps via the context dict +3. **Choose error policies carefully**: Use SKIP for optional steps, RAISE for critical ones +4. **Use conditional execution**: Avoid empty/no-op workflow executions +5. **Log step transitions**: Makes debugging pipeline failures easier +6. **Generate deterministic execution IDs**: Enables idempotency and replay diff --git a/.claude/skills/workflows/references/workflow_testing.md b/.claude/skills/workflows/references/workflow_testing.md new file mode 100644 index 000000000..44ceb543e --- /dev/null +++ b/.claude/skills/workflows/references/workflow_testing.md @@ -0,0 +1,173 @@ +# Workflow Registration Testing + +Ensure all workflow classes are properly registered in workers to prevent production errors. + +## Overview + +A common production issue is creating a new workflow class but forgetting to register it in the worker. + +## Test All Decorated Classes Are Registered + +The most comprehensive test - scans the codebase for all `@workflow.define` decorators: + +```python +import ast +from pathlib import Path + +from pytest_mock import MockerFixture + +from core.workflows.worker import get_workflows + +# Root directory for scanning +PROJECT_ROOT = Path(__file__).parent.parent.parent.parent + + +def _find_workflow_decorated_classes() -> dict[str, str]: + """Scan codebase for all classes decorated with @workflows.workflow.define. + + Returns: + Dict mapping class names to their file paths. + """ + decorated_classes: dict[str, str] = {} + + # Directories containing workflow definitions + scan_dirs = [ + PROJECT_ROOT / "core", + PROJECT_ROOT / "plugins", + PROJECT_ROOT / "workflows", + ] + + def is_workflow_decorator(node: ast.expr) -> bool: + """Check if decorator matches @workflows.workflow.define pattern.""" + # Handle @workflows.workflow.define(...) + if isinstance(node, ast.Call): + node = node.func + + # Matches `import mistralai.workflows as workflows` → workflows.workflow.define + if isinstance(node, ast.Attribute) and node.attr == "define": + if isinstance(node.value, ast.Attribute) and node.value.attr == "workflow": + if isinstance(node.value.value, ast.Name): + return node.value.value.id == "workflows" + return False + + def scan_file(file_path: Path) -> None: + """Parse a Python file and extract workflow-decorated class names.""" + try: + source = file_path.read_text() + tree = ast.parse(source) + except (SyntaxError, UnicodeDecodeError): + return + + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef): + for decorator in node.decorator_list: + if is_workflow_decorator(decorator): + relative_path = str(file_path.relative_to(PROJECT_ROOT)) + decorated_classes[node.name] = relative_path + break + + for scan_path in scan_dirs: + if scan_path.is_file(): + scan_file(scan_path) + elif scan_path.is_dir(): + for py_file in scan_path.rglob("*.py"): + # Skip test files + if "/tests/" in str(py_file) or py_file.name.startswith("test_"): + continue + scan_file(py_file) + + return decorated_classes + + +def test_all_workflow_decorated_classes_are_registered(mocker: MockerFixture) -> None: + """Ensure all @workflow.define classes are registered in get_workflows(). + + This prevents the common error where a workflow is created but not + added to the worker registration. + """ + # Get workflows for dev mode + dev_workflows = get_workflows(dev_only=True) + + # Get dev environment workflows + mocker.patch("core.workflows.worker.settings.environment", "dev") + dev_env_workflows = get_workflows(dev_only=False) + + # Get production environment workflows + mocker.patch("core.workflows.worker.settings.environment", "production") + prod_env_workflows = get_workflows(dev_only=False) + + # Combine all registered workflows + registered_workflows = set(dev_workflows + dev_env_workflows + prod_env_workflows) + registered_names = {cls.__name__ for cls in registered_workflows} + + # Find all decorated classes in codebase + decorated_classes = _find_workflow_decorated_classes() + + # Check for missing registrations + missing = { + name: path + for name, path in decorated_classes.items() + if name not in registered_names + } + + if missing: + missing_details = "\n".join( + f" - {name} (defined in {path})" + for name, path in sorted(missing.items()) + ) + raise AssertionError( + f"The following workflow classes are decorated with @workflow.define " + f"but are NOT registered in get_workflows():\n{missing_details}\n\n" + f"Please add them to core/workflows/worker.py" + ) +``` + +## Unit Testing with `create_test_worker` + +The SDK provides `create_test_worker` in `mistralai.workflows.testing` for running workflows against an in-memory Temporal environment. It handles DI scoping, sandbox setup, and pre-registers event-emitting activities. + +### Fixtures + +Import the SDK fixtures in your `conftest.py`: + +```python +# conftest.py +from mistralai.workflows.testing.fixtures import * # noqa: F401,F403 +``` + +This provides `temporal_env` (in-memory Temporal with time-skipping) and auto-use fixtures for test config, DI cache clearing, and search attribute mocking. + +### Example + +```python +from mistralai.workflows.testing import create_test_worker + +async def test_my_workflow(temporal_env): + async with create_test_worker(temporal_env, [MyWorkflow], [my_activity]) as worker: + handle = await temporal_env.client.start_workflow( + "my-workflow", + {"input_field": "value"}, + id="test-id", + task_queue="test-task-queue", + ) + result = await handle.result() + assert result == {"output_field": "expected"} +``` + +## CI Integration + +Add to your CI pipeline: + +```yaml +# .github/workflows/test.yml +- name: Run workflow registration tests + run: pytest tests/core/workflows/test_worker.py -v +``` + +## Best Practices + +1. **Run tests in CI**: Catch missing registrations before deployment +2. **Test all environments**: Both dev and production workflow lists +3. **Skip test files**: Don't flag test workflow classes as missing +4. **Clear error messages**: Include file paths to help locate unregistered classes +6. **Mock environment settings**: Test both dev and production code paths diff --git a/.claude/skills/workflows/scripts/test_workflow.py b/.claude/skills/workflows/scripts/test_workflow.py new file mode 100644 index 000000000..45d1cd6bc --- /dev/null +++ b/.claude/skills/workflows/scripts/test_workflow.py @@ -0,0 +1,481 @@ +#!/usr/bin/env python3 +"""Workflow test runner -- starts a real worker, executes via the API, reports the result. + +Usage: + python test_workflow.py --input '{}' + python test_workflow.py --input '{}' --interactions '[{"choice": "WFL"}]' + python test_workflow.py --input '{}' --timeout 60 --workflow-name my-wf + +For interactive workflows (those extending InteractiveWorkflow), pass --interactions +with a JSON array. Each element is submitted in order to the next wait_for_input() call. + +Exit codes: 0 = passed, 1 = failed/timed out, 2 = bad arguments. +""" + +from __future__ import annotations + +import argparse +import asyncio +import importlib.util +import inspect +import json +import os +import sys +import traceback +from pathlib import Path +from typing import Any + +# SDK imports (deferred to avoid import errors when just running --help) +_sdk_imported = False + + +def _ensure_sdk(): + global _sdk_imported + if _sdk_imported: + return + global workflows, get_mistral_client, get_workflow_definition, BaseModel + import mistralai.workflows as _wf + from mistralai.workflows.client import get_mistral_client as _gmc + from mistralai.workflows.core.definition.workflow_definition import ( + get_workflow_definition as _gwd, + ) + from pydantic import BaseModel as _BM + + workflows = _wf + get_mistral_client = _gmc + get_workflow_definition = _gwd + BaseModel = _BM + _sdk_imported = True + + +# --------------------------------------------------------------------------- +# Discovery +# --------------------------------------------------------------------------- + + +def _import_module(file_path: Path) -> Any: + """Import a Python module from a filesystem path.""" + spec = importlib.util.spec_from_file_location(file_path.stem, file_path) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot create module spec for {file_path}") + mod = importlib.util.module_from_spec(spec) + sys.modules[file_path.stem] = mod + spec.loader.exec_module(mod) + return mod + + +def _find_workflow_classes(module: Any) -> list[type]: + """Return all @workflow.define classes in *module*.""" + return [ + obj + for _, obj in inspect.getmembers(module, inspect.isclass) + if hasattr(obj, "__workflows_workflow_def") + ] + + +def _workflow_name(cls: type) -> str: + return get_workflow_definition(cls).name + + +def _is_interactive(cls: type) -> bool: + return issubclass(cls, workflows.InteractiveWorkflow) + + +def _discover_workflow( + workflow_file: Path, name_override: str | None +) -> tuple[type, str, bool]: + """Find and select the workflow class. Returns (cls, name, interactive).""" + module = _import_module(workflow_file) + found = _find_workflow_classes(module) + + if not found: + raise SystemExit( + f"No workflow classes found in {workflow_file}. " + "Ensure the file has a class decorated with @workflow.define." + ) + + if name_override: + matches = [w for w in found if _workflow_name(w) == name_override] + if not matches: + available = ", ".join(_workflow_name(w) for w in found) + raise SystemExit( + f"Workflow '{name_override}' not found. Available: {available}" + ) + cls = matches[0] + else: + if len(found) > 1: + names = ", ".join(_workflow_name(w) for w in found) + print( + f"Multiple workflows found: {names}. Using the first one. " + "Pass --workflow-name to select.", + file=sys.stderr, + ) + cls = found[0] + + return cls, _workflow_name(cls), _is_interactive(cls) + + +# --------------------------------------------------------------------------- +# Input helpers +# --------------------------------------------------------------------------- + + +def _build_input(input_data: dict) -> dict | None: + """Return the input dict for the API client, or None if empty.""" + if not input_data: + return None + return input_data + + +# --------------------------------------------------------------------------- +# Interactions (for interactive workflows) +# --------------------------------------------------------------------------- + + +async def _poll_and_submit_interactions( + client: Any, + execution_id: str, + interactions: list[dict], + poll_timeout: float = 60.0, + poll_interval: float = 0.5, +) -> None: + """Poll __get_pending_inputs and submit each interaction response in order.""" + + class _Payload(BaseModel): + task_id: str + input: dict + + for i, response_data in enumerate(interactions, 1): + task_id = await _wait_for_pending_input( + client, execution_id, i, poll_timeout, poll_interval + ) + + print(f" Interaction {i}: submitting {json.dumps(response_data)}") + try: + payload = _Payload(task_id=task_id, input=response_data) + resp = await asyncio.wait_for( + client.workflows.executions.update_workflow_execution_async( + execution_id=execution_id, + name="__submit_input", + input=payload.model_dump(mode="json"), + ), + timeout=30, + ) + except asyncio.TimeoutError: + print( + f" Interaction {i}: update timed out " + "(workflow may have failed to process the input)", + file=sys.stderr, + ) + raise + + error = resp.result.get("error") if isinstance(resp.result, dict) else None + if error: + raise RuntimeError(f"Interaction {i} rejected: {error}") + print(f" Interaction {i}: accepted") + + +async def _wait_for_pending_input( + client: Any, + execution_id: str, + index: int, + timeout: float, + interval: float, +) -> str: + """Block until a pending input appears, return its task_id.""" + start = asyncio.get_event_loop().time() + while True: + try: + resp = await client.workflows.executions.query_workflow_execution_async( + execution_id=execution_id, name="__get_pending_inputs" + ) + pending = resp.result.get("pending_inputs", []) + if pending: + task_id = pending[0]["task_id"] + label = pending[0].get("label", "") + print( + f" Interaction {index}: pending input found " + f"(task={task_id[:8]}..., label={label!r})" + ) + return task_id + except Exception: + pass + + if asyncio.get_event_loop().time() - start > timeout: + raise TimeoutError( + f"Timeout waiting for pending input #{index} ({timeout}s)" + ) + await asyncio.sleep(interval) + + +# --------------------------------------------------------------------------- +# Execution +# --------------------------------------------------------------------------- + + +async def _execute_with_retry( + client: Any, wf_name: str, input_dict: dict | None, retries: int = 10 +) -> Any: + """Start the workflow, retrying on registration-propagation errors.""" + for attempt in range(retries): + try: + return await client.workflows.execute_workflow_async( + workflow_identifier=wf_name, + input=input_dict, + ) + except Exception: + if attempt == retries - 1: + raise + await asyncio.sleep(1) + + +async def _await_result( + client: Any, + execution_id: str, + interactions: list[dict] | None, + interactive: bool, + timeout: int, +) -> dict: + """Wait for workflow completion, submitting interactions if needed. + + Uses asyncio.wait with FIRST_EXCEPTION so that interaction errors + (e.g. validation failures) surface immediately instead of blocking + until the overall timeout. + """ + if not (interactive and interactions): + final = await asyncio.wait_for( + client.workflows.wait_for_workflow_completion_async( + execution_id, polling_interval=2 + ), + timeout=timeout, + ) + return final.result + + # Run interactions and completion polling concurrently. + interaction_task = asyncio.create_task( + _poll_and_submit_interactions( + client, execution_id, interactions, poll_timeout=timeout + ) + ) + completion_task = asyncio.create_task( + client.workflows.wait_for_workflow_completion_async( + execution_id, polling_interval=2 + ) + ) + + done, pending = await asyncio.wait( + [interaction_task, completion_task], + timeout=timeout, + return_when=asyncio.FIRST_EXCEPTION, + ) + + for t in pending: + t.cancel() + try: + await t + except (asyncio.CancelledError, Exception): + pass + + if not done: + raise asyncio.TimeoutError() + + # Surface errors -- interaction errors take priority. + if interaction_task in done and interaction_task.exception(): + raise interaction_task.exception() + if completion_task in done and completion_task.exception(): + raise completion_task.exception() + + if completion_task in done: + return completion_task.result().result + + raise asyncio.TimeoutError() + + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +# How long to wait after starting the worker before executing. +# The worker needs time to register the workflow with the API and +# start polling the task queue. 8s is empirically reliable. +_WORKER_READY_DELAY = 8 + + +async def run_workflow( + workflow_file: Path, + input_data: dict, + timeout_seconds: int, + workflow_name_override: str | None, + interactions: list[dict] | None = None, +) -> dict: + """Start a real worker, execute the workflow, and return the result.""" + _ensure_sdk() + + # -- discover -- + workflow_cls, wf_name, interactive = _discover_workflow( + workflow_file, workflow_name_override + ) + + print(f"Workflow: {wf_name} ({workflow_cls.__name__})") + print(f"Interactive: {interactive}") + print(f"Input: {json.dumps(input_data)}") + if interactions: + print(f"Interactions: {len(interactions)} response(s) queued") + print(f"Timeout: {timeout_seconds}s") + print() + + if interactive and not interactions: + print( + "WARNING: Interactive workflow but no --interactions provided.\n" + " The workflow will hang at wait_for_input().\n", + file=sys.stderr, + ) + + # -- API client -- + client = get_mistral_client() + + # -- start worker -- + print("Starting worker...") + worker_task = await workflows.run_worker([workflow_cls], detach=True) + if worker_task is None: + raise RuntimeError("run_worker(detach=True) returned None") + print("Worker started.") + + print("Waiting for worker to be ready...", end="", flush=True) + await asyncio.sleep(_WORKER_READY_DELAY) + print(" ready.\n") + + execution_id: str | None = None + try: + # -- execute -- + execution = await _execute_with_retry( + client, wf_name, _build_input(input_data) + ) + execution_id = execution.execution_id + print(f"Execution: {execution_id}") + print(f"Status: {execution.status}\n") + + # -- wait for result -- + return await _await_result( + client, execution_id, interactions, interactive, timeout_seconds + ) + + except asyncio.TimeoutError: + if execution_id: + print(f"\nTerminating execution {execution_id}...", file=sys.stderr) + try: + await client.workflows.executions.terminate_workflow_execution_async( + execution_id=execution_id + ) + print("Execution terminated.", file=sys.stderr) + except Exception as e: + print(f"Failed to terminate: {e}", file=sys.stderr) + raise + + finally: + if worker_task and not worker_task.done(): + worker_task.cancel() + try: + await worker_task + except asyncio.CancelledError: + pass + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Run a workflow with a real worker and the Workflows API.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "workflow_file", type=Path, + help="Path to the Python file containing the workflow.", + ) + parser.add_argument( + "--input", required=True, dest="input_json", + help="JSON string with the workflow input.", + ) + parser.add_argument( + "--timeout", type=int, default=30, + help="Max seconds before the workflow is killed (default: 30).", + ) + parser.add_argument( + "--workflow-name", default=None, + help="Workflow name (if the file contains multiple workflows).", + ) + parser.add_argument( + "--interactions", default=None, dest="interactions_json", + help=( + "JSON array of interaction responses for interactive workflows. " + 'Example: \'[{"choice": "WFL"}]\'' + ), + ) + return parser.parse_args() + + +def _parse_json(raw: str, label: str) -> Any: + try: + return json.loads(raw) + except json.JSONDecodeError as e: + print(f"Error: invalid JSON in {label}: {e}", file=sys.stderr) + raise SystemExit(2) + + +def main() -> None: + args = _parse_args() + + if not args.workflow_file.is_file(): + print(f"Error: {args.workflow_file} does not exist.", file=sys.stderr) + raise SystemExit(2) + + input_data = _parse_json(args.input_json, "--input") + + interactions = None + if args.interactions_json: + interactions = _parse_json(args.interactions_json, "--interactions") + if not isinstance(interactions, list): + print("Error: --interactions must be a JSON array.", file=sys.stderr) + raise SystemExit(2) + + # Load .env if present. + try: + from dotenv import load_dotenv + load_dotenv(override=True) + except ImportError: + pass + + # Add workflow's directory to sys.path for relative imports. + parent = str(args.workflow_file.resolve().parent) + if parent not in sys.path: + sys.path.insert(0, parent) + + try: + result = asyncio.run( + run_workflow( + args.workflow_file.resolve(), + input_data, + args.timeout, + args.workflow_name, + interactions, + ) + ) + print("PASSED") + print(json.dumps(result, indent=2, default=str)) + except SystemExit: + raise + except asyncio.TimeoutError: + print("FAILED: workflow timed out", file=sys.stderr) + raise SystemExit(1) + except Exception: + print("FAILED:", file=sys.stderr) + traceback.print_exc() + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 6e05ee3bb..cb51e9306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }] maintainers = [{ name = "Pipelex staff", email = "oss@pipelex.com" }] license = "MIT" readme = "README.md" -requires-python = ">=3.10,<3.15" +requires-python = ">=3.12,<3.15" classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", @@ -85,9 +85,10 @@ google-genai = [ huggingface = ["huggingface_hub>=0.23,<1.0.0"] linkup = ["linkup-sdk>=0.12.0"] mistralai = ["mistralai>=1.12.0"] +mistralai-workflows = ["mistralai-workflows>=3.3.0"] dynamodb = ["boto3>=1.34.131"] s3 = ["boto3>=1.34.131", "aioboto3>=13.4.0"] -temporal = ["temporalio==1.23.0", "aiohttp>=3.9.0"] +temporal = ["temporalio==1.24.0", "aiohttp>=3.9.0"] docs = [ "mike>=2.1.3", "mkdocs>=1.6.1", diff --git a/uv.lock b/uv.lock index a3991d4da..64ca01331 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10, <3.15" +requires-python = ">=3.12, <3.15" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", @@ -8,13 +8,9 @@ resolution-markers = [ "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.11'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [[package]] @@ -23,8 +19,7 @@ version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "packaging" }, { name = "psutil" }, { name = "pyyaml" }, @@ -97,7 +92,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, - { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, @@ -106,40 +100,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/85/cebc47ee74d8b408749073a1a46c6fcba13d170dc8af7e61996c6c9394ac/aiohttp-3.13.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:02222e7e233295f40e011c1b00e3b0bd451f22cf853a0304c3595633ee47da4b", size = 750547, upload-time = "2026-03-31T21:56:30.024Z" }, - { url = "https://files.pythonhosted.org/packages/05/98/afd308e35b9d3d8c9ec54c0918f1d722c86dc17ddfec272fcdbcce5a3124/aiohttp-3.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bace460460ed20614fa6bc8cb09966c0b8517b8c58ad8046828c6078d25333b5", size = 503535, upload-time = "2026-03-31T21:56:31.935Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/926c183e06b09d5270a309eb50fbde7b09782bfd305dec1e800f329834fb/aiohttp-3.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f546a4dc1e6a5edbb9fd1fd6ad18134550e096a5a43f4ad74acfbd834fc6670", size = 497830, upload-time = "2026-03-31T21:56:33.654Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d6/f47d1c690f115a5c2a5e8938cce4a232a5be9aac5c5fb2647efcbbbda333/aiohttp-3.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c86969d012e51b8e415a8c6ce96f7857d6a87d6207303ab02d5d11ef0cad2274", size = 1682474, upload-time = "2026-03-31T21:56:35.513Z" }, - { url = "https://files.pythonhosted.org/packages/01/44/056fd37b1bb52eac760303e5196acc74d9d546631b035704ae5927f7b4ac/aiohttp-3.13.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b6f6cd1560c5fa427e3b6074bb24d2c64e225afbb7165008903bd42e4e33e28a", size = 1655259, upload-time = "2026-03-31T21:56:37.843Z" }, - { url = "https://files.pythonhosted.org/packages/91/9f/78eb1a20c1c28ae02f6a3c0f4d7b0dcc66abce5290cadd53d78ce3084175/aiohttp-3.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:636bc362f0c5bbc7372bc3ae49737f9e3030dbce469f0f422c8f38079780363d", size = 1736204, upload-time = "2026-03-31T21:56:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/de/6c/d20d7de23f0b52b8c1d9e2033b2db1ac4dacbb470bb74c56de0f5f86bb4f/aiohttp-3.13.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a7cbeb06d1070f1d14895eeeed4dac5913b22d7b456f2eb969f11f4b3993796", size = 1826198, upload-time = "2026-03-31T21:56:41.378Z" }, - { url = "https://files.pythonhosted.org/packages/2f/86/a6f3ff1fd795f49545a7c74b2c92f62729135d73e7e4055bf74da5a26c82/aiohttp-3.13.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca9ef7517fd7874a1a08970ae88f497bf5c984610caa0bf40bd7e8450852b95", size = 1681329, upload-time = "2026-03-31T21:56:43.374Z" }, - { url = "https://files.pythonhosted.org/packages/fb/68/84cd3dab6b7b4f3e6fe9459a961acb142aaab846417f6e8905110d7027e5/aiohttp-3.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:019a67772e034a0e6b9b17c13d0a8fe56ad9fb150fc724b7f3ffd3724288d9e5", size = 1560023, upload-time = "2026-03-31T21:56:45.031Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/db61b64b0249e30f954a65ab4cb4970ced57544b1de2e3c98ee5dc24165f/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f34ecee82858e41dd217734f0c41a532bd066bcaab636ad830f03a30b2a96f2a", size = 1652372, upload-time = "2026-03-31T21:56:47.075Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/e96988a6c982d047810c772e28c43c64c300c943b0ed5c1c0c4ce1e1027c/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4eac02d9af4813ee289cd63a361576da36dba57f5a1ab36377bc2600db0cbb73", size = 1662031, upload-time = "2026-03-31T21:56:48.835Z" }, - { url = "https://files.pythonhosted.org/packages/b7/26/a56feace81f3d347b4052403a9d03754a0ab23f7940780dada0849a38c92/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4beac52e9fe46d6abf98b0176a88154b742e878fdf209d2248e99fcdf73cd297", size = 1708118, upload-time = "2026-03-31T21:56:50.833Z" }, - { url = "https://files.pythonhosted.org/packages/78/6e/b6173a8ff03d01d5e1a694bc06764b5dad1df2d4ed8f0ceec12bb3277936/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c180f480207a9b2475f2b8d8bd7204e47aec952d084b2a2be58a782ffcf96074", size = 1548667, upload-time = "2026-03-31T21:56:52.81Z" }, - { url = "https://files.pythonhosted.org/packages/16/13/13296ffe2c132d888b3fe2c195c8b9c0c24c89c3fa5cc2c44464dc23b22e/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2837fb92951564d6339cedae4a7231692aa9f73cbc4fb2e04263b96844e03b4e", size = 1724490, upload-time = "2026-03-31T21:56:54.541Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1f1c287f4a79782ef36e5a6e62954c85343bc30470d862d30bd5f26c9fa2/aiohttp-3.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9010032a0b9710f58012a1e9c222528763d860ba2ee1422c03473eab47703e7", size = 1667109, upload-time = "2026-03-31T21:56:56.21Z" }, - { url = "https://files.pythonhosted.org/packages/ef/42/8461a2aaf60a8f4ea4549a4056be36b904b0eb03d97ca9a8a2604681a500/aiohttp-3.13.5-cp310-cp310-win32.whl", hash = "sha256:7c4b6668b2b2b9027f209ddf647f2a4407784b5d88b8be4efcc72036f365baf9", size = 439478, upload-time = "2026-03-31T21:56:58.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/71/06956304cb5ee439dfe8d86e1b2e70088bd88ed1ced1f42fb29e5d855f0e/aiohttp-3.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:cd3db5927bf9167d5a6157ddb2f036f6b6b0ad001ac82355d43e97a4bde76d76", size = 462047, upload-time = "2026-03-31T21:57:00.257Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, - { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, - { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, - { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, - { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, - { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, - { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, - { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, @@ -280,7 +240,6 @@ name = "anyio" version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -302,21 +261,18 @@ wheels = [ name = "astroid" version = "4.0.4" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] 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/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 = "async-timeout" -version = "5.0.1" +name = "asynciolimiter" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/95/e419550994947b564302c6d6641462d1c154e535df7e6a4bedc2801ec6d5/asynciolimiter-1.2.0.tar.gz", hash = "sha256:ac1a237c3dbd3c33041c9f9fc1a687c8e6b5268af69da84399967173a344b265", size = 8343, upload-time = "2025-03-18T08:44:43.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, + { url = "https://files.pythonhosted.org/packages/ee/91/2fd273f5c9d041e987cb03f487722355527c412870ece6f803fe2799f2a7/asynciolimiter-1.2.0-py3-none-any.whl", hash = "sha256:0d255de14459f961c8baee7f91e114a6ffb9ce5615e9b19c0049e8ae7e2ac84c", size = 8372, upload-time = "2025-03-18T08:44:42.2Z" }, ] [[package]] @@ -355,24 +311,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - -[[package]] -name = "backports-strenum" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/c7/2ed54c32fed313591ffb21edbd48db71e68827d43a61938e5a0bc2b6ec91/backports_strenum-1.3.1.tar.gz", hash = "sha256:77c52407342898497714f0596e86188bb7084f89063226f4ba66863482f42414", size = 7257, upload-time = "2023-12-09T14:36:40.937Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/50/56cf20e2ee5127b603b81d5a69580a1a325083e2b921aa8f067da83927c0/backports_strenum-1.3.1-py3-none-any.whl", hash = "sha256:cdcfe36dc897e2615dc793b7d3097f54d359918fc448754a517e6f23044ccf83", size = 8304, upload-time = "2023-12-09T14:36:39.905Z" }, -] - [[package]] name = "backrefs" version = "7.0" @@ -410,21 +348,9 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, - { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, - { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, @@ -464,7 +390,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/bb/3766f6dcb09369732b575c49bd26458ee133348b07ae4b61c2feec1cae0d/boto3_stubs-1.43.4.tar.gz", hash = "sha256:09e6dfdc3b7db02b7c9bab2dae748b0179acad7ced72ee62b79a5e80fd588caa", size = 102637, upload-time = "2026-05-05T19:53:33.221Z" } wheels = [ @@ -524,31 +449,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, @@ -603,38 +503,6 @@ version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, @@ -741,35 +609,6 @@ version = "7.13.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, - { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, - { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, - { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, - { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, - { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, - { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, - { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, - { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, - { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, - { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, @@ -848,18 +687,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cryptography" version = "48.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } wheels = [ @@ -905,12 +738,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, - { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, ] [[package]] @@ -918,13 +745,9 @@ name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ef/184aa775e970fc089942cd9ec6302e6e44679d4c14549c6a7ea45bf7f798/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6f3682ec3c4769326aafc67c2ba669d97d688d0b7e63e659d36d2f8b72f32d6", size = 6329075, upload-time = "2026-03-11T00:12:32.319Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, @@ -953,37 +776,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -999,7 +822,6 @@ dependencies = [ { name = "jinja2" }, { name = "pydantic" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/9c/5379be8daf9ff2fbbb9a7efcf68c037b089e5b76f360c4f9ca730916e2ee/datamodel_code_generator-0.56.1.tar.gz", hash = "sha256:697abd90cc4eb2c65f130be79a83a24746c3f2d0e15e6eb9dbf17b96784449be", size = 840372, upload-time = "2026-04-16T17:09:53.537Z" } wheels = [ @@ -1068,8 +890,7 @@ dependencies = [ { name = "jsonref" }, { name = "jsonschema" }, { name = "latex2mathml" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, { name = "pillow" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -1103,8 +924,7 @@ dependencies = [ { name = "docling-core" }, { name = "huggingface-hub" }, { name = "jsonlines" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pillow" }, { name = "pydantic" }, { name = "rtree" }, @@ -1132,14 +952,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a7/b8/e68f8ec44692d2f913210dd46cb3e7e6e1959053bb05d5c94c5331010f3c/docling_parse-5.10.1.tar.gz", hash = "sha256:10a3d2ba211134f6d1fa9b6be8ef690eb0b1a03b043473a3ef8408ad7b4a857a", size = 6651696, upload-time = "2026-04-24T15:02:19.106Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/ba/0520b9b74c73dc6c970e23ddb54d900c4195290fa49bfb35530f9619efdf/docling_parse-5.10.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:88eaa801a44d518c110e50d381beefe480f7f7d6485779947f4ed918d55be000", size = 9110525, upload-time = "2026-04-24T15:01:22.767Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ff/cbff26277fb93839456b0f4d163c6d55fbe02ccc9089cddc67c14fc301bf/docling_parse-5.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:643ad0b95db00acc674dc7941f6314b60aa2de8a35c15f04d5c42d89a75a1414", size = 9833595, upload-time = "2026-04-24T15:01:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/01/c5/efcdb4e6d4bb581c448a3981b7983eaddf862cb2eac9e0cda980e39aa9f2/docling_parse-5.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bdf7df582e5e7d50dbc61138545a6dcc6c00878db9521d0fa58aa1ef4bb26ea", size = 10106212, upload-time = "2026-04-24T15:01:28.039Z" }, - { url = "https://files.pythonhosted.org/packages/53/c6/8028ca5e196e19deab3299e49e0701a656e355b0860f33c76cbc2a9ff843/docling_parse-5.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:4427ec4a5cc42a92aaab9104375180df70f2c1206c0261ba33dc9640f3744837", size = 10909812, upload-time = "2026-04-24T15:01:30.671Z" }, - { url = "https://files.pythonhosted.org/packages/38/5d/6ed2d12f7c9db1f714093306141e437b6a199b72c11bae82d9980bd22a23/docling_parse-5.10.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8a4b52d966b9b9e8400290e1400c549cf73e52e1636f7345e2b8b7f7e10c04c4", size = 9111237, upload-time = "2026-04-24T15:01:33.57Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ff/022881eb3ec824527851e6a7640d99251815b89772e1a14087cd7e47e0bd/docling_parse-5.10.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31912b95f29db264c9c6b32b4884160ab01ea919307f860493f562cf8c7f9ea1", size = 9780134, upload-time = "2026-04-24T15:01:36.388Z" }, - { url = "https://files.pythonhosted.org/packages/26/c2/f9e956aacbf9c88ac228b5abf08962ba2ec88e2b3810703f212e8d0f6df7/docling_parse-5.10.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f70d4364fbc9dd62cd4f75e0cff93e3618e06bf96686575f7d2e8c5a6fa4f823", size = 10158640, upload-time = "2026-04-24T15:01:38.797Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/2e3bb89731e354a1a565194267b70a245f52a45f3590b4757a01ede69c39/docling_parse-5.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3c90a8b28a9ce012e55e3dd2ae632fc735e5827759cd36fbb8cbbb7da361aec3", size = 10910184, upload-time = "2026-04-24T15:01:42.134Z" }, { url = "https://files.pythonhosted.org/packages/3e/4a/27e213493bac0877a030f030d44152ba9ef676aebc5890f4dd3e8037592e/docling_parse-5.10.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8f58e1bf1c6cdf1bfe0594f0903b4b9c33dfc3c2dbba61681f8533158ed640a6", size = 9112936, upload-time = "2026-04-24T15:01:44.878Z" }, { url = "https://files.pythonhosted.org/packages/ee/b3/85737cecca0e5ed9dc13370e78862054075f64eb988024069296898ec741/docling_parse-5.10.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59880a29231083c17a73533e09abc0610f10a99343762d795de4ceac5b15dfbf", size = 9780913, upload-time = "2026-04-24T15:01:47.626Z" }, { url = "https://files.pythonhosted.org/packages/17/fa/11dd3328ab708143a291ae53d388a5170095a90b8dc8428e5274e2c09194/docling_parse-5.10.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:478ada90c52b704a04a3c8b4171e3385bb8b5b2f02b9d57c7a5bb06d9cac34fa", size = 10159004, upload-time = "2026-04-24T15:01:50.366Z" }, @@ -1185,8 +997,7 @@ standard = [ { name = "huggingface-hub" }, { name = "lxml" }, { name = "marko" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "openpyxl" }, { name = "pillow" }, { name = "polyfactory" }, @@ -1197,8 +1008,7 @@ standard = [ { name = "rapidocr" }, { name = "rich" }, { name = "rtree" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy" }, { name = "torch" }, { name = "torchvision" }, { name = "typer" }, @@ -1232,18 +1042,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/22/fdc2e30d43ff853720042fa15baa3e6122722be1a7950a98233ebb55cd71/eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8", size = 6063, upload-time = "2025-12-02T11:51:41.665Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -1306,38 +1104,6 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, - { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, - { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, - { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, - { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, - { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, - { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, - { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, - { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, - { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, - { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, - { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, @@ -1534,16 +1300,6 @@ version = "1.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, - { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, - { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, @@ -1559,8 +1315,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, ] [[package]] @@ -1776,15 +1530,6 @@ google-genai = [ { name = "jsonref" }, ] -[[package]] -name = "invoke" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, -] - [[package]] name = "isort" version = "7.0.0" @@ -1812,31 +1557,6 @@ version = "0.13.0" source = { registry = "https://pypi.org/simple" } 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/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, - { 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" }, @@ -1893,10 +1613,6 @@ wheels = [ { 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" }, @@ -1930,6 +1646,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpath-python" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/db/2f4ecc24da35c6142b39c353d5b7c16eef955cc94b35a48d3fa47996d7c3/jsonpath_python-1.1.5.tar.gz", hash = "sha256:ceea2efd9e56add09330a2c9631ea3d55297b9619348c1055e5bfb9cb0b8c538", size = 87352, upload-time = "2026-03-17T06:16:40.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/50/1a313fb700526b134c71eb8a225d8b83be0385dbb0204337b4379c698cef/jsonpath_python-1.1.5-py3-none-any.whl", hash = "sha256:a60315404d70a65e76c9a782c84e50600480221d94a58af47b7b4d437351cb4b", size = 14090, upload-time = "2026-03-17T06:16:39.152Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + [[package]] name = "jsonref" version = "1.1.0" @@ -1993,31 +1739,6 @@ version = "0.10.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/cb/c1945e506893b5b8577fb45a60c80e3ffe4a82092a04a6f29b0b951d9a24/librt-0.10.0.tar.gz", hash = "sha256:1aba1e8aa4e3307a7be68a74149545fde7451964dc0235a8bec5704a17bdda42", size = 191799, upload-time = "2026-05-05T16:31:23.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/18/827e5c1262a88c2602e86f99aee0f288ffea3280dbd2ff448858ef9dc6e9/librt-0.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dc99f9642100b86e5f6bb14cdc9970009e31a9ef7d64df6704b7018451524a3", size = 76461, upload-time = "2026-05-05T16:29:00.422Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/54254e30287f5a5abec6fef22d976987476e966be5fdff51fe8c2d5d73d1/librt-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8298cedfcfaff3790000bd057aaaa3df1b0ab54cf7b48eeab16184cbb1bc66b9", size = 79740, upload-time = "2026-05-05T16:29:01.926Z" }, - { url = "https://files.pythonhosted.org/packages/4c/20/e93264b52113669d98d3b63ff94d4ce0c4dd49ae0503f1788440a884e5f0/librt-0.10.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7dbe312dbf76468255b79a7ba311236fde620f2f7055fc09d421e31340314e", size = 243472, upload-time = "2026-05-05T16:29:03.373Z" }, - { url = "https://files.pythonhosted.org/packages/35/ad/34a5141178e8b18a4cfa45d1a0d523c84397e2abd5d06fea2d846da687e8/librt-0.10.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:56ed90c48c19249012dadfd79a1bc13bd5168ea60a70722d330a3a600c0b1852", size = 232073, upload-time = "2026-05-05T16:29:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/97/1f/67240e910cd9f9ab1498c1470738345fc29dce5dc9719db1e0e09d1e861f/librt-0.10.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d74ca0f4b2b09c117f913d4df01f6b934dff8a271096b35167d5264a31649f0", size = 256956, upload-time = "2026-05-05T16:29:06.516Z" }, - { url = "https://files.pythonhosted.org/packages/22/50/3a2b3482c27d607f6e8216d913c6bc592b9a2141d96990309452340a78e3/librt-0.10.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8eb2daa9375f93c0e55ff5e44a4bbe98f39e5fe52e1abf9c97acb67743b61bf8", size = 250593, upload-time = "2026-05-05T16:29:08.324Z" }, - { url = "https://files.pythonhosted.org/packages/e7/1c/07dba133d79f93322fa17514062f1a2a50d6bdfb7baec4acf78193d7fad1/librt-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7b09b90e634e6dff57978cd358070046071e2b120501f10787aeb35425f504f6", size = 263582, upload-time = "2026-05-05T16:29:09.866Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ac/033f2c6d6ab0b48f15f02e5bf065521b11a51922806017f8b6274df30d69/librt-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2cf22fd379d60c739b800d4295ed34045f8b04aa8df9c12bd2f8f43f7fe672b7", size = 259307, upload-time = "2026-05-05T16:29:11.675Z" }, - { url = "https://files.pythonhosted.org/packages/6e/10/679046cd75d5a52c0104c890d8f69574ef4e619c683e59c15584d03a2457/librt-0.10.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:74c798793fcf29a84d442278ebe0bb1fff79fe58ac4106eeff7019cbba861423", size = 257342, upload-time = "2026-05-05T16:29:13.14Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d5/dbaac9c0884f78a53dda22b9ec92bb788e1400e762ed7623fa96928c8da5/librt-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f1573401e8dbe6c26511fe027620b0fb30ae9a7ab814e02e510626b8b5f9c", size = 280141, upload-time = "2026-05-05T16:29:14.922Z" }, - { url = "https://files.pythonhosted.org/packages/cc/81/71f18cf8eb340d9fda011498870910f6a8697aeb50833005d3d8107653fd/librt-0.10.0-cp310-cp310-win32.whl", hash = "sha256:e1428275f5fe3d4db6822e58d8b005a5b28ffca55e8433ebc051247fbe46429f", size = 62257, upload-time = "2026-05-05T16:29:16.226Z" }, - { url = "https://files.pythonhosted.org/packages/df/52/6bcebc2f870c4836bcb372be885fae7f17a1d25037d3a8250ef79fbe0124/librt-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:0708e9408f585b0f065081680583a577652099680ccf820c7538904322b679c3", size = 70321, upload-time = "2026-05-05T16:29:17.41Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/1472717d2325adacc8d335ba2e4078015c09d75b599f3cf48e967b3d306e/librt-0.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:01b4500ca3a625450c032a9142a8e843923ce263fa8a92ad1b38927cabe2fe72", size = 76045, upload-time = "2026-05-05T16:29:18.731Z" }, - { url = "https://files.pythonhosted.org/packages/a6/31/bfe32355d4b369aef3d7aa442df663bb5558c2ffa2de286cb2956346bc24/librt-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b7e42d1b3e300d20bfc87e72ffd62f0a92a2cb3c35f7bf90df90c9d2a49f74c", size = 79466, upload-time = "2026-05-05T16:29:20.052Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f1/83f8a2c715ba2cac9b7387a5a5cea25f717f7184320cfe48b36bed9c58e9/librt-0.10.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8ef7b8c61ce3a1b597cd3e15348ff1574325165c2e7ce09a718154cde2a7950", size = 242283, upload-time = "2026-05-05T16:29:21.596Z" }, - { url = "https://files.pythonhosted.org/packages/cc/94/c3a4ce94857f0004a542f86662806383611858f522722db58efaec0a1472/librt-0.10.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e73c84f72d1fa0d6eaa7a1930b436ba8d2c90c58d77bfabb09995a69ad35f6c0", size = 230735, upload-time = "2026-05-05T16:29:23.335Z" }, - { url = "https://files.pythonhosted.org/packages/d1/41/e962bb26c7728eb7b3a69e490d0c800fd9968a6970e390c1f18ddb56093d/librt-0.10.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9728cb98713bd862fb8f4fd6a642d1896c86058a41d77c70f3d5cee75e725275", size = 256606, upload-time = "2026-05-05T16:29:24.91Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/4e46a707b1ecc993fd691071623b9beab89703a63bd21cc7807e06c28209/librt-0.10.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:648b7e941d20acd72f9652115e0e53facd98156d61f9ebf7a812bdef8bdccea9", size = 249739, upload-time = "2026-05-05T16:29:26.648Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f5/dc5b7eb294656ad23d4ff4cf8514208d54fe1026b909d726a0dc026689c9/librt-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c3e33747c068e86a9007c20fdb777eb5ba8d3d19136d7812f88e69a713041b6f", size = 261414, upload-time = "2026-05-05T16:29:28.702Z" }, - { url = "https://files.pythonhosted.org/packages/58/e4/990ed8d12c7f114ac8f8ccd47f7d9bd9704ef61acfcb1df4a05047da7710/librt-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d509c745bf7e77d1107cf05e6abb249dc03fad13eb39f2286a49deedaeb2bcd7", size = 256614, upload-time = "2026-05-05T16:29:30.357Z" }, - { url = "https://files.pythonhosted.org/packages/60/eb/52d2726c7fb22818507dc3cc166c8f36dd4a4b68a7be67f12006ac8777c1/librt-0.10.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:786ad5a15e99d0e0e74f3adbeecc198a5ac58f340be07e984723d1e0074838de", size = 255144, upload-time = "2026-05-05T16:29:32.106Z" }, - { url = "https://files.pythonhosted.org/packages/bc/df/bd5591a78f7531fce4b6eb9962aadc6adc9560a01570442a884b6e554abe/librt-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:075582d877a97ee3d8e77bda3689dbe617b14f6469224a2d80b4b6c38e3951aa", size = 279121, upload-time = "2026-05-05T16:29:33.688Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/7c2b838dfc89a1762dd156d8b0c39848a7a2845d725a50be5a6e021fb8ba/librt-0.10.0-cp311-cp311-win32.whl", hash = "sha256:75ecdc3f5a90065aa2af2e574706c5495adc392520762dcf10b1aa716f0b8090", size = 62593, upload-time = "2026-05-05T16:29:35.152Z" }, - { url = "https://files.pythonhosted.org/packages/91/19/22ff572981049a9d436a083dbea1572d0f5dc068b7353637d2dd9977c8f1/librt-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:b6f6084884131d8a52cb9d7095ff2aa52c1e786d9fdaefab1fb4515415e9e083", size = 70914, upload-time = "2026-05-05T16:29:36.407Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/1697cc64f4a5c7e9bce55e99c6d234a346beaedaefcd1e2ca90dd285f98c/librt-0.10.0-cp311-cp311-win_arm64.whl", hash = "sha256:0140bd62151160047e89b2730cb6f8506cdac5127baa1afb9231e4dd3fe7f681", size = 61176, upload-time = "2026-05-05T16:29:37.62Z" }, { url = "https://files.pythonhosted.org/packages/12/8e/cbb5b6f6e45e65c10a42449a69eaccc44d73e6a081ea752fbc5221c6dc1c/librt-0.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b4b58a44b407e91f633dafee008de9ddea6aa2a555ed94929c099260910bd0ba", size = 77327, upload-time = "2026-05-05T16:29:38.919Z" }, { url = "https://files.pythonhosted.org/packages/e9/3d/8233cbee8e99e6a8992f02bfc2dec8d787509566a511d1fde2574ee7473f/librt-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:950b79b11762531bdf45a9df909d2f9a2a8445c70c88665c01d14c8511a27dc5", size = 79971, upload-time = "2026-05-05T16:29:40.96Z" }, { url = "https://files.pythonhosted.org/packages/87/6f/5264b298cef2b72fc97d2dde56c66181eda35204bf5dcd1ed0c3d0a0a782/librt-0.10.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4538453f51be197633b425912c150e25b0667252d3741c53e8368176d98d9d37", size = 246559, upload-time = "2026-05-05T16:29:42.701Z" }, @@ -2091,38 +1812,6 @@ version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, - { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, - { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, - { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, - { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, - { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, - { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, - { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, - { url = "https://files.pythonhosted.org/packages/a7/23/851cfa33b6b38adb628e45ad51fb27105fa34b2b3ba9d1d4aa7a9428dfe0/lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e", size = 5068101, upload-time = "2026-04-18T04:32:21.437Z" }, - { url = "https://files.pythonhosted.org/packages/b0/38/41bf99c2023c6b79916ba057d83e9db21d642f473cac210201222882d38b/lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512", size = 5002573, upload-time = "2026-04-18T04:32:25.373Z" }, - { url = "https://files.pythonhosted.org/packages/c2/20/053aa10bdc39747e1e923ce2d45413075e84f70a136045bb09e5eaca41d3/lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c", size = 5202816, upload-time = "2026-04-18T04:32:29.393Z" }, - { url = "https://files.pythonhosted.org/packages/9a/da/bc710fad8bf04b93baee752c192eaa2210cd3a84f969d0be7830fea55802/lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5", size = 5329999, upload-time = "2026-04-18T04:32:34.019Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/bf035dedbdf7fab49411aa52e4236f3445e98d38647d85419e6c0d2806b9/lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289", size = 4659643, upload-time = "2026-04-18T04:32:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/5c/4f/22be31f33727a5e4c7b01b0a874503026e50329b259d3587e0b923cf964b/lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a", size = 5265963, upload-time = "2026-04-18T04:32:41.881Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2b/d44d0e5c79226017f4ab8c87a802ebe4f89f97e6585a8e4166dffcdd7b6e/lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3", size = 5045444, upload-time = "2026-04-18T04:32:44.512Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c3/3f034fec1594c331a6dbf9491238fdcc9d66f68cc529e109ec75b97197e1/lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9", size = 4712703, upload-time = "2026-04-18T04:32:47.16Z" }, - { url = "https://files.pythonhosted.org/packages/12/16/0b83fccc158218aca75a7aa33e97441df737950734246b9fffa39301603d/lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11", size = 5252745, upload-time = "2026-04-18T04:32:50.427Z" }, - { url = "https://files.pythonhosted.org/packages/dd/ee/12e6c1b39a77666c02eaa77f94a870aaf63c4ac3a497b2d52319448b01c6/lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4", size = 5226822, upload-time = "2026-04-18T04:32:53.437Z" }, - { url = "https://files.pythonhosted.org/packages/34/20/c7852904858b4723af01d2fc14b5d38ff57cb92f01934a127ebd9a9e51aa/lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3", size = 3594026, upload-time = "2026-04-18T04:27:31.903Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/d60c732b56da5085175c07c74b2df4e6d181b0c9a61e1691474f06ef4b39/lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7", size = 4025114, upload-time = "2026-04-18T04:27:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/c2/df/c84dcc175fd690823436d15b41cb920cd5ba5e14cd8bfb00949d5903b320/lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39", size = 3667742, upload-time = "2026-04-18T04:27:38.45Z" }, { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, @@ -2195,12 +1884,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, - { url = "https://files.pythonhosted.org/packages/f2/88/55143966481409b1740a3ac669e611055f49efd68087a5ce41582325db3e/lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842", size = 3930134, upload-time = "2026-04-18T04:32:35.008Z" }, - { url = "https://files.pythonhosted.org/packages/b5/97/28b985c2983938d3cb696dd5501423afb90a8c3e869ef5d3c62569282c0f/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c", size = 4210749, upload-time = "2026-04-18T04:36:03.626Z" }, - { url = "https://files.pythonhosted.org/packages/29/67/dfab2b7d58214921935ccea7ce9b3df9b7d46f305d12f0f532ac7cf6b804/lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de", size = 4318463, upload-time = "2026-04-18T04:36:06.309Z" }, - { url = "https://files.pythonhosted.org/packages/32/a2/4ac7eb32a4d997dd352c32c32399aae27b3f268d440e6f9cfa405b575d2f/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635", size = 4251124, upload-time = "2026-04-18T04:36:09.056Z" }, - { url = "https://files.pythonhosted.org/packages/33/ef/d6abd850bb4822f9b720cfe36b547a558e694881010ff7d012191e8769c6/lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037", size = 4401758, upload-time = "2026-04-18T04:36:11.803Z" }, - { url = "https://files.pythonhosted.org/packages/40/44/3ee09a5b60cb44c4f2fbc1c9015cfd6ff5afc08f991cab295d3024dcbf2d/lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace", size = 3508860, upload-time = "2026-04-18T04:32:48.619Z" }, ] [[package]] @@ -2239,28 +1922,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, @@ -2364,23 +2025,47 @@ wheels = [ [[package]] name = "mistralai" -version = "1.12.4" +version = "2.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "eval-type-backport" }, { name = "httpx" }, - { name = "invoke" }, + { name = "jsonpath-python" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, { name = "pydantic" }, { name = "python-dateutil" }, - { name = "pyyaml" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/12/c3476c53e907255b5f485f085ba50dd9a84b40fe662e9a888d6ded26fa7b/mistralai-1.12.4.tar.gz", hash = "sha256:e52b53bab58025dcd208eeac13e3c3df5778d4112eeca1f08124096c7738929f", size = 243129, upload-time = "2026-02-20T17:55:13.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/f0/80dfabf224be4419c6c112f3950676f5af3dcde582d225c03ca0196a4b32/mistralai-2.4.4.tar.gz", hash = "sha256:cd8a27a230e5458b62237a6c4f7b52f5be86909fbc18694360ceb21dac932eda", size = 420936, upload-time = "2026-04-30T12:26:38.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/74/0f6188190e79eef4363754c71414c1a791537b1e506cbee615bb581cb05f/mistralai-2.4.4-py3-none-any.whl", hash = "sha256:43dd3b1f0f4f960a723359165c4da0d035590b27c050028322934fe4f189f14e", size = 990545, upload-time = "2026-04-30T12:26:40.702Z" }, +] + +[[package]] +name = "mistralai-workflows" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asynciolimiter" }, + { name = "httpx" }, + { name = "jsonpatch" }, + { name = "mistralai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation-asyncio" }, + { name = "opentelemetry-instrumentation-httpx" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "structlog" }, + { name = "temporalio" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/ab/b7952d7f9ec11eb326b6ff3912d9378a7fa0b09767d7ec1c862bcfc95564/mistralai_workflows-3.3.0.tar.gz", hash = "sha256:e462bbed75840d9099728c39bd92f7a562f88e4cc0c97305c369cfa4e5019613", size = 505490, upload-time = "2026-04-23T16:02:14.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/f9/98d825105c450b9c67c27026caa374112b7e466c18331601d02ca278a01b/mistralai-1.12.4-py3-none-any.whl", hash = "sha256:7b69fcbc306436491ad3377fbdead527c9f3a0ce145ec029bf04c6308ff2cca6", size = 509321, upload-time = "2026-02-20T17:55:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/17ed0974d2ea5e75dd9690b35178cf0b51b32b3b9156d2d6375da7239b0d/mistralai_workflows-3.3.0-py3-none-any.whl", hash = "sha256:bedd37e16ef5d95eb9551415a5a2bfb5c92d6b2ae8c0e92883d0870da7b49645", size = 390405, upload-time = "2026-04-23T16:02:13.19Z" }, ] [[package]] @@ -2573,23 +2258,6 @@ version = "1.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, - { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, @@ -2633,11 +2301,9 @@ name = "mthds" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-strenum", marker = "python_full_version < '3.11'" }, { name = "httpx" }, { name = "pydantic" }, { name = "semantic-version" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, { name = "typing-extensions" }, ] @@ -2650,47 +2316,8 @@ wheels = [ name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] 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/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, - { 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" }, @@ -2793,12 +2420,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/b6/10832f96b499690854e574360be342a282f5f7dba58eff791299ff6c0637/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:02e5c35d7d6cd2bdc89c1858867f7bde4012837411023a4696c148c1bdd7c80e", size = 135131, upload-time = "2026-01-19T06:47:20.479Z" }, - { url = "https://files.pythonhosted.org/packages/99/50/faef2d8106534b0dc4a0b772668a1a99682696ebf17d3c0f13f2ed6a656a/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79576c02d1207ec405b00cabf2c643c36070800cca433860e14539df7818b2aa", size = 135131, upload-time = "2026-01-19T06:47:21.879Z" }, - { url = "https://files.pythonhosted.org/packages/94/b1/0b71d18b76bf423c2e8ee00b31db37d17297ab3b4db44e188692afdca628/multiprocess-0.70.19-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6b6d78d43a03b68014ca1f0b7937d965393a670c5de7c29026beb2258f2f896", size = 135134, upload-time = "2026-01-19T06:47:23.262Z" }, - { url = "https://files.pythonhosted.org/packages/7e/aa/714635c727dbfc251139226fa4eaf1b07f00dc12d9cd2eb25f931adaf873/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7", size = 144743, upload-time = "2026-01-19T06:47:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e1/155f6abf5e6b5d9cef29b6d0167c180846157a4aca9b9bee1a217f67c959/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e", size = 144738, upload-time = "2026-01-19T06:47:26.636Z" }, - { url = "https://files.pythonhosted.org/packages/af/cb/f421c2869d75750a4f32301cc20c4b63fab6376e9a75c8e5e655bdeb3d9b/multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45", size = 144741, upload-time = "2026-01-19T06:47:27.985Z" }, { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, @@ -2815,23 +2436,10 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] 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/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { 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" }, @@ -2862,36 +2470,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] 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/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, @@ -2899,14 +2481,14 @@ wheels = [ [[package]] name = "nexus-rpc" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/f2/d54f5c03d8f4672ccc0875787a385f53dcb61f98a8ae594b5620e85b9cb3/nexus_rpc-1.3.0.tar.gz", hash = "sha256:e56d3b57b60d707ce7a72f83f23f106b86eca1043aa658e44582ab5ff30ab9ad", size = 75650, upload-time = "2025-12-08T22:59:13.002Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/d5/cd1ffb202b76ebc1b33c1332a3416e55a39929006982adc2b1eb069aaa9b/nexus_rpc-1.4.0.tar.gz", hash = "sha256:3b8b373d4865671789cc43623e3dc0bcbf192562e40e13727e17f1c149050fba", size = 82367, upload-time = "2026-02-25T22:01:34.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/74/0afd841de3199c148146c1d43b4bfb5605b2f1dc4c9a9087fe395091ea5a/nexus_rpc-1.3.0-py3-none-any.whl", hash = "sha256:aee0707b4861b22d8124ecb3f27d62dafbe8777dc50c66c91e49c006f971b92d", size = 28873, upload-time = "2025-12-08T22:59:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/11/52/6327a5f4fda01207205038a106a99848a41c83e933cd23ea2cab3d2ebc6c/nexus_rpc-1.4.0-py3-none-any.whl", hash = "sha256:14c953d3519113f8ccec533a9efdb6b10c28afef75d11cdd6d422640c40b3a49", size = 29645, upload-time = "2026-02-25T22:01:33.122Z" }, ] [[package]] @@ -2918,102 +2500,12 @@ wheels = [ { 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 = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - [[package]] name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, - { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, - { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, - { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, - { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, - { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, - { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, @@ -3067,13 +2559,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, - { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, - { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, ] [[package]] @@ -3117,7 +2602,7 @@ name = "nvidia-cudnn-cu13" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, @@ -3129,7 +2614,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -3159,9 +2644,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -3173,7 +2658,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -3271,8 +2756,7 @@ name = "opencv-python" version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] wheels = [ { 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" }, @@ -3299,32 +2783,32 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.41.1" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.41.1" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz", hash = "sha256:0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", size = 20409, upload-time = "2026-04-24T13:15:40.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl", hash = "sha256:10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", size = 18366, upload-time = "2026-04-24T13:15:18.917Z" }, + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.41.1" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -3335,48 +2819,156 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz", hash = "sha256:4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", size = 24139, upload-time = "2026-04-24T13:15:42.977Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-asyncio" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/7f/a108a095fa56c715559a9e441f7fb7b0741cbfb4e363acfb69a7f12648b4/opentelemetry_instrumentation_asyncio-0.60b1.tar.gz", hash = "sha256:0ddb8ada367c7102662c42433740779a055eace08d65079c86ef65e75d1a66b3", size = 14050, upload-time = "2025-12-11T13:36:48.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl", hash = "sha256:1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", size = 22673, upload-time = "2026-04-24T13:15:21.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/50/6be23a04742c69e53bfc417134ee0955d0572ce1540242023de73aa74e18/opentelemetry_instrumentation_asyncio-0.60b1-py3-none-any.whl", hash = "sha256:9f326c6947a23dccc02a07ab432b0c62b62bf7312a7608c38771014217199316", size = 14747, upload-time = "2025-12-11T13:35:42.567Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-httpx" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/08/11208bcfcab4fc2023252c3f322aa397fd9ad948355fea60f5fc98648603/opentelemetry_instrumentation_httpx-0.60b1.tar.gz", hash = "sha256:a506ebaf28c60112cbe70ad4f0338f8603f148938cb7b6794ce1051cd2b270ae", size = 20611, upload-time = "2025-12-11T13:37:01.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/59/b98e84eebf745ffc75397eaad4763795bff8a30cbf2373a50ed4e70646c5/opentelemetry_instrumentation_httpx-0.60b1-py3-none-any.whl", hash = "sha256:f37636dd742ad2af83d896ba69601ed28da51fa4e25d1ab62fde89ce413e275b", size = 15701, upload-time = "2025-12-11T13:36:04.56Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.41.1" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz", hash = "sha256:4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", size = 45670, upload-time = "2026-04-24T13:15:49.768Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl", hash = "sha256:0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", size = 72076, upload-time = "2026-04-24T13:15:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.41.1" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz", hash = "sha256:724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", size = 230180, upload-time = "2026-04-24T13:15:50.991Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl", hash = "sha256:edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", size = 180213, upload-time = "2026-04-24T13:15:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.62b1" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz", hash = "sha256:c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", size = 145750, upload-time = "2026-04-24T13:15:52.236Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl", hash = "sha256:cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", size = 231620, upload-time = "2026-04-24T13:15:35.454Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/fc/c47bb04a1d8a941a4061307e1eddfa331ed4d0ab13d8a9781e6db256940a/opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6", size = 11053, upload-time = "2025-12-11T13:37:25.115Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/5c/d3f1733665f7cd582ef0842fb1d2ed0bc1fba10875160593342d22bba375/opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199", size = 8947, upload-time = "2025-12-11T13:36:37.151Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, ] [[package]] @@ -3397,103 +2989,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, -] -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/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, - { 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]] name = "pandas" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, - { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, - { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, @@ -3550,28 +3056,6 @@ version = "12.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f", size = 5354355, upload-time = "2026-04-01T14:42:15.402Z" }, - { url = "https://files.pythonhosted.org/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97", size = 4695871, upload-time = "2026-04-01T14:42:18.234Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff", size = 6269734, upload-time = "2026-04-01T14:42:20.608Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec", size = 8076080, upload-time = "2026-04-01T14:42:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136", size = 6382236, upload-time = "2026-04-01T14:42:25.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c", size = 7070220, upload-time = "2026-04-01T14:42:28.68Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3", size = 6493124, upload-time = "2026-04-01T14:42:31.579Z" }, - { url = "https://files.pythonhosted.org/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa", size = 7194324, upload-time = "2026-04-01T14:42:34.615Z" }, - { url = "https://files.pythonhosted.org/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032", size = 6376363, upload-time = "2026-04-01T14:42:37.19Z" }, - { url = "https://files.pythonhosted.org/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5", size = 7083523, upload-time = "2026-04-01T14:42:39.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024", size = 2463318, upload-time = "2026-04-01T14:42:42.063Z" }, - { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, - { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, - { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, - { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, @@ -3633,13 +3117,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, - { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, - { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] [[package]] @@ -3648,7 +3125,6 @@ version = "0.26.4" source = { editable = "." } dependencies = [ { name = "aiofiles" }, - { name = "backports-strenum", marker = "python_full_version < '3.11'" }, { name = "datamodel-code-generator" }, { name = "filetype" }, { name = "httpx" }, @@ -3658,8 +3134,7 @@ dependencies = [ { name = "kajson" }, { name = "markdown" }, { name = "mthds" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, { name = "openai" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, @@ -3754,6 +3229,9 @@ linkup = [ mistralai = [ { name = "mistralai" }, ] +mistralai-workflows = [ + { name = "mistralai-workflows" }, +] s3 = [ { name = "aioboto3" }, { name = "boto3" }, @@ -3798,6 +3276,7 @@ requires-dist = [ { name = "markdown", specifier = ">=3.6" }, { name = "mike", marker = "extra == 'docs'", specifier = ">=2.1.3" }, { name = "mistralai", marker = "extra == 'mistralai'", specifier = ">=1.12.0" }, + { name = "mistralai-workflows", marker = "extra == 'mistralai-workflows'", specifier = ">=3.3.0" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1" }, { name = "mkdocs-glightbox", marker = "extra == 'docs'", specifier = ">=0.4.0" }, { name = "mkdocs-llmstxt-md", marker = "extra == 'docs'", specifier = ">=0.2.0" }, @@ -3836,7 +3315,7 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.13" }, { name = "semantic-version", specifier = ">=2.10.0" }, { name = "shortuuid", specifier = ">=1.0.13" }, - { name = "temporalio", marker = "extra == 'temporal'", specifier = "==1.23.0" }, + { name = "temporalio", marker = "extra == 'temporal'", specifier = "==1.24.0" }, { name = "tomli", specifier = ">=2.3.0" }, { name = "tomlkit", specifier = ">=0.13.2" }, { name = "typer", specifier = ">=0.16.0" }, @@ -3847,7 +3326,7 @@ requires-dist = [ { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250326" }, { name = "typing-extensions", specifier = ">=4.13.2" }, ] -provides-extras = ["anthropic", "bedrock", "docling", "fal", "gcp-storage", "google", "google-genai", "huggingface", "linkup", "mistralai", "dynamodb", "s3", "temporal", "docs", "dev"] +provides-extras = ["anthropic", "bedrock", "docling", "fal", "gcp-storage", "google", "google-genai", "huggingface", "linkup", "mistralai", "mistralai-workflows", "dynamodb", "s3", "temporal", "docs", "dev"] [[package]] name = "pipelex-tools" @@ -3937,36 +3416,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, - { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, - { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, - { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, @@ -4136,18 +3585,6 @@ 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/8a/9f/a10173d32ecc2ce19a04d018163f3ca22a04c0c6ad03b464dcd32f9152a8/pyclipper-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73", size = 264510, upload-time = "2025-12-01T13:14:46.551Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c2/5490ddc4a1f7ceeaa0258f4266397e720c02db515b2ca5bc69b85676f697/pyclipper-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447", size = 139498, upload-time = "2025-12-01T13:14:48.31Z" }, - { url = "https://files.pythonhosted.org/packages/3b/0a/bea9102d1d75634b1a5702b0e92982451a1eafca73c4845d3dbe27eba13d/pyclipper-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d", size = 970974, upload-time = "2025-12-01T13:14:49.799Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/097f8776d5b3a10eb7b443b632221f4ed825d892e79e05682f4b10a1a59c/pyclipper-1.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc", size = 943315, upload-time = "2025-12-01T13:14:51.266Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4d/17d6a3f1abf0f368d58f2309e80ee3761afb1fd1342f7780ab32ba4f0b1d/pyclipper-1.4.0-cp310-cp310-win32.whl", hash = "sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a", size = 95286, upload-time = "2025-12-01T13:14:52.922Z" }, - { url = "https://files.pythonhosted.org/packages/53/ca/b30138427ed122ec9b47980b943164974a2ec606fa3f71597033b9a9f9a6/pyclipper-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4", size = 104227, upload-time = "2025-12-01T13:14:54.013Z" }, - { 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" }, @@ -4170,7 +3607,6 @@ wheels = [ { 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]] @@ -4206,35 +3642,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, @@ -4295,22 +3702,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] @@ -4353,7 +3748,6 @@ dependencies = [ { name = "isort" }, { name = "mccabe" }, { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } @@ -4422,12 +3816,10 @@ version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -4439,7 +3831,6 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -4453,7 +3844,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -4567,16 +3958,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, @@ -4600,26 +3981,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] -[[package]] -name = "pytz" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, -] - [[package]] name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, @@ -4637,24 +4003,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -4713,8 +4061,7 @@ version = "3.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorlog" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "omegaconf" }, { name = "opencv-python" }, { name = "pillow" }, @@ -4749,39 +4096,6 @@ version = "2026.4.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/59/fd98f8fd54b3feaa76a855324c676c17668c5a1121ec91b7ec96b01bf865/regex-2026.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:74fa82dcc8143386c7c0392e18032009d1db715c25f4ba22d23dc2e04d02a20f", size = 489403, upload-time = "2026-04-03T20:52:39.742Z" }, - { url = "https://files.pythonhosted.org/packages/6c/64/d0f222f68e3579d50babf0e4fcc9c9639ef0587fecc00b15e1e46bfc32fa/regex-2026.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a85b620a388d6c9caa12189233109e236b3da3deffe4ff11b84ae84e218a274f", size = 291208, upload-time = "2026-04-03T20:52:42.943Z" }, - { url = "https://files.pythonhosted.org/packages/16/7f/3fab9709b0b0060ba81a04b8a107b34147cd14b9c5551b772154d6505504/regex-2026.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2895506ebe32cc63eeed8f80e6eae453171cfccccab35b70dc3129abec35a5b8", size = 289214, upload-time = "2026-04-03T20:52:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/14/bc/f5dcf04fd462139dcd75495c02eee22032ef741cfa151386a39c3f5fc9b5/regex-2026.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6780f008ee81381c737634e75c24e5a6569cc883c4f8e37a37917ee79efcafd9", size = 785505, upload-time = "2026-04-03T20:52:46.35Z" }, - { url = "https://files.pythonhosted.org/packages/37/36/8a906e216d5b4de7ec3788c1d589b45db40c1c9580cd7b326835cfc976d4/regex-2026.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88e9b048345c613f253bea4645b2fe7e579782b82cac99b1daad81e29cc2ed8e", size = 852129, upload-time = "2026-04-03T20:52:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/a5/bb/bad2d79be0917a6ef31f5e0f161d9265cb56fd90a3ae1d2e8d991882a48b/regex-2026.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be061028481186ba62a0f4c5f1cc1e3d5ab8bce70c89236ebe01023883bc903b", size = 899578, upload-time = "2026-04-03T20:52:50.61Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b9/7cd0ceb58cd99c70806241636640ae15b4a3fe62e22e9b99afa67a0d7965/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2228c02b368d69b724c36e96d3d1da721561fb9cc7faa373d7bf65e07d75cb5", size = 793634, upload-time = "2026-04-03T20:52:53Z" }, - { url = "https://files.pythonhosted.org/packages/2c/fb/c58e3ea40ed183806ccbac05c29a3e8c2f88c1d3a66ed27860d5cad7c62d/regex-2026.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0540e5b733618a2f84e9cb3e812c8afa82e151ca8e19cf6c4e95c5a65198236f", size = 786210, upload-time = "2026-04-03T20:52:54.713Z" }, - { url = "https://files.pythonhosted.org/packages/54/a9/53790fc7a6c948a7be2bc7214fd9cabdd0d1ba561b0f401c91f4ff0357f0/regex-2026.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cf9b1b2e692d4877880388934ac746c99552ce6bf40792a767fd42c8c99f136d", size = 769930, upload-time = "2026-04-03T20:52:56.825Z" }, - { url = "https://files.pythonhosted.org/packages/e3/3c/29ca44729191c79f5476538cd0fa04fa2553b3c45508519ecea4c7afa8f6/regex-2026.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:011bb48bffc1b46553ac704c975b3348717f4e4aa7a67522b51906f99da1820c", size = 774892, upload-time = "2026-04-03T20:52:58.934Z" }, - { url = "https://files.pythonhosted.org/packages/3e/db/6ae74ef8a4cfead341c367e4eed45f71fb1aaba35827a775eed4f1ba4f74/regex-2026.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8512fcdb43f1bf18582698a478b5ab73f9c1667a5b7548761329ef410cd0a760", size = 848816, upload-time = "2026-04-03T20:53:00.684Z" }, - { url = "https://files.pythonhosted.org/packages/53/9a/f7f2c1c6b610d7c6de1c3dc5951effd92c324b1fde761af2044b4721020f/regex-2026.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:867bddc63109a0276f5a31999e4c8e0eb7bbbad7d6166e28d969a2c1afeb97f9", size = 758363, upload-time = "2026-04-03T20:53:02.155Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/e5386d393bbf8b43c8b084703a46d635e7b2bdc6e0f5909a2619ea1125f1/regex-2026.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1b9a00b83f3a40e09859c78920571dcb83293c8004079653dd22ec14bbfa98c7", size = 837122, upload-time = "2026-04-03T20:53:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/cc78710ea2e60b10bacfcc9beb18c67514200ab03597b3b2b319995785c2/regex-2026.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e355be718caf838aa089870259cf1776dc2a4aa980514af9d02c59544d9a8b22", size = 782140, upload-time = "2026-04-03T20:53:05.608Z" }, - { url = "https://files.pythonhosted.org/packages/a2/5f/c7bcba41529105d6c2ca7080ecab7184cd00bee2e1ad1fdea80e618704ea/regex-2026.4.4-cp310-cp310-win32.whl", hash = "sha256:33bfda9684646d323414df7abe5692c61d297dbb0530b28ec66442e768813c59", size = 266225, upload-time = "2026-04-03T20:53:07.342Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/a745729c2c49354ec4f4bce168f29da932ca01b4758227686cc16c7dde1b/regex-2026.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:0709f22a56798457ae317bcce42aacee33c680068a8f14097430d9f9ba364bee", size = 278393, upload-time = "2026-04-03T20:53:08.65Z" }, - { url = "https://files.pythonhosted.org/packages/87/8b/4327eeb9dbb4b098ebecaf02e9f82b79b6077beeb54c43d9a0660cf7c44c/regex-2026.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:ee9627de8587c1a22201cb16d0296ab92b4df5cdcb5349f4e9744d61db7c7c98", size = 270470, upload-time = "2026-04-03T20:53:10.018Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, - { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, - { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, - { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, - { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, - { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, - { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, - { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, - { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, @@ -4938,35 +4252,6 @@ 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/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { 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" }, @@ -5040,18 +4325,6 @@ wheels = [ { 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]] @@ -5128,112 +4401,24 @@ wheels = [ { 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" }, - { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, ] [package.optional-dependencies] torch = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "packaging" }, { name = "torch" }, ] -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, -] - [[package]] name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] 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" }, @@ -5292,24 +4477,6 @@ version = "0.4.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b3/1a/ce7768c1bff5cf07e3e12f904d54a63c3f85f78e3b299c0b561839d0238e/selectolax-0.4.8.tar.gz", hash = "sha256:cd703165b9a346be255e2ca5b4219e01009911977ac8a474d8ccb7e32e9a4fae", size = 4875521, upload-time = "2026-05-04T15:10:44.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/89/0933738d60eb6e0d872e68c7d473d6efe9e99a0f7eec8a970febb4c319c5/selectolax-0.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2166f893420119151bb644f79c145f6d75f1f0435f98b071d9e663f273890551", size = 2206038, upload-time = "2026-05-04T15:08:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/e6/1d/a8457a0fb898d9803aabdbe2028841f03889ba1d95771164c1bdce9fd1ef/selectolax-0.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4542a35f5ea993ae8bdb5850346f65848794afabc83976e576dcab07a6699020", size = 2256699, upload-time = "2026-05-04T15:08:48.989Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a9/a30c621ae96d50c8ef15de3ca0f35c4b2878d5e1e270dcb6a9d1c1299e85/selectolax-0.4.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c65d50ab4654c55a3fa17db17cae39405a688693aad0681d8cd8bf8c1b01b588", size = 2339298, upload-time = "2026-05-04T15:08:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/8e/85/f4f06a30e63bbbaa42685122b7b0718c43ccb37b6e58f8a160b563b6fc37/selectolax-0.4.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:239fb539f67a43cbd543d48a09314eb955b5f3fe1b9ede4e1a31feb7ccfdd6b0", size = 2389018, upload-time = "2026-05-04T15:08:52.791Z" }, - { url = "https://files.pythonhosted.org/packages/40/47/50b43a9047f230f34e1851cc7beb63123376f40c7eb1d48e24b2eabbd5eb/selectolax-0.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6753fe6132c4dfbc984ce9b48777f7f7282c63b3943e558459a6b8847da962e", size = 2344273, upload-time = "2026-05-04T15:08:54.748Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/b34159afe1fcd69a866ab5484d3b73b299f1fbaf27726536881bdc6499e3/selectolax-0.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:059056df901766278018a1064dd4e49a6111a01fef02a7e668352fe716c80c84", size = 2397489, upload-time = "2026-05-04T15:08:56.597Z" }, - { url = "https://files.pythonhosted.org/packages/72/46/ac5703fafdf12c4b00c76a9c3b7d4a0d9fa1878865fa865d8997203a7e88/selectolax-0.4.8-cp310-cp310-win32.whl", hash = "sha256:c7a10112c6f96f3db34e757d8e82e7441fd76c7361dc0d8f19a9c383bd4afd26", size = 1764576, upload-time = "2026-05-04T15:08:58.455Z" }, - { url = "https://files.pythonhosted.org/packages/e6/97/6bdb3f557242838394eb971fba0b0212f982537dc354aadd4624638f6750/selectolax-0.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:4be603bbe70f6c036b189bd2a7227ab2fd7d47d360c2282a3623712cf3e2f23d", size = 1856985, upload-time = "2026-05-04T15:09:00.123Z" }, - { url = "https://files.pythonhosted.org/packages/b8/65/623fe0540be1e8df84074c0be9136e5be786f17ece3a7ee33bfc2e3ba12d/selectolax-0.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:903abf55ffcd2b5a8f8918925e9c532ffeae09b752c39371569a5350dc1c1157", size = 1811972, upload-time = "2026-05-04T15:09:01.795Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d8/fd9c0dc417b0e131153925af812be188f6e29139998bddfe3f9f5b275733/selectolax-0.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e334eb5d1a8ed41c539d5fbc170bfbecbaa5312c6f3358cb36b606565f748a8c", size = 2211435, upload-time = "2026-05-04T15:09:03.826Z" }, - { url = "https://files.pythonhosted.org/packages/82/74/d2c2a2448d7355fedcf24583ab9c3083bf0dbd13fb9bad98a166517ac65f/selectolax-0.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:241b99d10ebf1df8b7b57fd90a3479abfd987ef04acb6253c4d3571b570346c9", size = 2262297, upload-time = "2026-05-04T15:09:05.426Z" }, - { url = "https://files.pythonhosted.org/packages/62/fc/25939114a8706a8b17b066c594da929fbb9dcdee1fd2bb078befa5ebf2f2/selectolax-0.4.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1269dcb740b7a1eaefabce6b189324a5bd9c93a39b25b54a888b4ea0cf10290d", size = 2342920, upload-time = "2026-05-04T15:09:07.043Z" }, - { url = "https://files.pythonhosted.org/packages/63/bb/3d3e0a9350a81f3cf38e0bf69c4ad0c17e2f4bfe142c98202294fe0cfa92/selectolax-0.4.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8414beca1e821cd260d94c8fe687f9a8ae4ef765e2b4710d8bd233ec32cde26", size = 2393930, upload-time = "2026-05-04T15:09:09.088Z" }, - { url = "https://files.pythonhosted.org/packages/af/8a/a955323a52cbae720e515bb7c3b4042c506b28fedf79a25501a0750d8140/selectolax-0.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbaac0b708e539358343305534be59cc47875f2b62b50b33af50f5a1ecf7bff2", size = 2347656, upload-time = "2026-05-04T15:09:11.128Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c5/b141cd0f136a6a41d945869d27e303a96ee76e2feb70045b70754949dd65/selectolax-0.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dc2781ef2a6ac8aeed10f729aaafff056053e2430e0effa291a8cc74fdf2fdee", size = 2403976, upload-time = "2026-05-04T15:09:13.031Z" }, - { url = "https://files.pythonhosted.org/packages/40/a4/fb9830ab5973c957f28d651968d3d2e32729bb4e6e43cb313bd2c7b7a035/selectolax-0.4.8-cp311-cp311-win32.whl", hash = "sha256:ed66d2531b58ff7117c84f8d6d62d2d0d8a426f04d10ca45bd3744c7af71acd1", size = 1763669, upload-time = "2026-05-04T15:09:14.601Z" }, - { url = "https://files.pythonhosted.org/packages/74/c4/b8fadb6d43b7c1de1a703f4b064fb9eee65a8342bec0605cc65e02c68fdd/selectolax-0.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:f96959cecb10e70fd93e89e2c4522cfa671f5443427f4ccc12285604b6239c85", size = 1859140, upload-time = "2026-05-04T15:09:16.511Z" }, - { url = "https://files.pythonhosted.org/packages/06/60/2c38d657b6fcdbdb42eb0e37c87fc328f517c6890d6eb196cdb69ab32355/selectolax-0.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:787370345153cde7cd65f2271a00d7608b77993e1951f2f941a3c2ef92b942e0", size = 1811847, upload-time = "2026-05-04T15:09:18.095Z" }, { url = "https://files.pythonhosted.org/packages/12/3c/9cf255f11d04cf203b61f5a85fb273bc85778c3ab5d2ad98ce892f7df3b4/selectolax-0.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbcd4cb837dec4b16a3ab6a8b2ec4388ea20c050092b68536dc9a60ce8f19b56", size = 2236812, upload-time = "2026-05-04T15:09:20.193Z" }, { url = "https://files.pythonhosted.org/packages/9c/6b/4c343f767fa61131fc03b3de278dfcff024485996d7d6c917b55b0f9ce16/selectolax-0.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1cdcf946dd46e11640ca0a0361945c3ed65627ba4bd219ccf84a53fab28c072", size = 2287194, upload-time = "2026-05-04T15:09:22.15Z" }, { url = "https://files.pythonhosted.org/packages/9f/7d/5a47a0d102709297b6b09473a58c7e9727955a0d6e0c8eba9b27574e3680/selectolax-0.4.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:074254670a8a00b36202ab0cd99ce8fb2a25b3b2f10072891a35a6a85c53f679", size = 2369192, upload-time = "2026-05-04T15:09:23.768Z" }, @@ -5384,27 +4551,10 @@ name = "shapely" version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { 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 = [ - { url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" }, - { url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" }, - { url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" }, - { url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" }, - { url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" }, - { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, - { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, - { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, - { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, - { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, - { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, @@ -5492,6 +4642,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -5515,22 +4674,21 @@ wheels = [ [[package]] name = "temporalio" -version = "1.23.0" +version = "1.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, { name = "protobuf" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, { name = "types-protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/48/ba7413e2fab8dcd277b9df00bafa572da24e9ca32de2f38d428dc3a2825c/temporalio-1.23.0.tar.gz", hash = "sha256:72750494b00eb73ded9db76195e3a9b53ff548780f73d878ec3f807ee3191410", size = 1933051, upload-time = "2026-02-18T17:48:22.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b1/7d9b3104ab7994e7d49e765b92495aaff44810b1e066c874c284a93ebd55/temporalio-1.24.0.tar.gz", hash = "sha256:e534e2e71b4a721193ec4ff3dae521146d093554bd47a64f5605d4ca33e56718", size = 2040485, upload-time = "2026-03-23T15:33:33.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/71/26c8f21dca9092201b3b9cb7aff42460b4864b5999aa4c6a4343ac66f1fd/temporalio-1.23.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6b69ac8d75f2d90e66f4edce4316f6a33badc4a30b22efc50e9eddaa9acdc216", size = 12311037, upload-time = "2026-02-18T17:47:47.628Z" }, - { url = "https://files.pythonhosted.org/packages/ec/47/43102816139f2d346680cb7cc1e53da5f6968355ac65b4d35d4edbfca896/temporalio-1.23.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1bbbb2f9c3cdd09451565163f6d741e51f109694c49435d475fdfa42b597219d", size = 11821906, upload-time = "2026-02-18T17:47:55.314Z" }, - { url = "https://files.pythonhosted.org/packages/00/b0/899ff28464a0e17adf17476bdfac8faf4ea41870358ff2d14737e43f9e66/temporalio-1.23.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf6570e0ee696f99a38d855da4441a890c7187357c16505ed458ac9ef274ed70", size = 12063601, upload-time = "2026-02-18T17:48:03.994Z" }, - { url = "https://files.pythonhosted.org/packages/ed/17/b8c6d2ec3e113c6a788322513a5ff635bdd54b3791d092ed0e273467748a/temporalio-1.23.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b82d6cca54c9f376b50e941dd10d12f7fe5b692a314fb087be72cd2898646a79", size = 12394579, upload-time = "2026-02-18T17:48:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b7/f9ef7fd5ee65aef7d59ab1e95cb1b45df2fe49c17e3aa4d650ae3322f015/temporalio-1.23.0-cp310-abi3-win_amd64.whl", hash = "sha256:43c3b99a46dd329761a256f3855710c4a5b322afc879785e468bdd0b94faace6", size = 12834494, upload-time = "2026-02-18T17:48:19.071Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/30517c21d6155bce1c3dc0e420db48da0231230dbc683f40ab6d5fe22b37/temporalio-1.24.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7f11e7b4f4d09bafba499b43188353e23dc128b1fe3f3160014476e3dce70760", size = 12223918, upload-time = "2026-03-23T15:33:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/11aa103bde794524008c1850a84e06cde98698395ca1f8b12e1bd2390aa8/temporalio-1.24.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:5cff75a0ca922575b808a7fca1b0de38f6eea061f49e026664b8be9d5bb06ab8", size = 11708887, upload-time = "2026-03-23T15:33:11.67Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/774b56100e6bb94e3757ec96fb5c2bc62d42defc7d6de0ee35a12273827a/temporalio-1.24.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee7c13b6724dd0c304aa846aecf6da72a8550f4ade40a0a7f6dcc1c92ef35710", size = 12028303, upload-time = "2026-03-23T15:33:18.022Z" }, + { url = "https://files.pythonhosted.org/packages/e5/91/c05d0e9c2432fe8b1ea0d6fae321866ee49a320ad5e494e6ec9424ca5c28/temporalio-1.24.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa71b9bfa42f951dd04ade97ce7f92ecedee8903047b4b41b122bb8cbd87a337", size = 12375155, upload-time = "2026-03-23T15:33:24.234Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/5c939e4609c164c8690a3b5a135eb828d531de8ef63ff447a2a439c0b0fb/temporalio-1.24.0-cp310-abi3-win_amd64.whl", hash = "sha256:52f6833647eceddbebcc376e2ea663a9f73b2b3a42675f503aeb27c98fd4daeb", size = 12720174, upload-time = "2026-03-23T15:33:30.826Z" }, ] [[package]] @@ -5575,10 +4733,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, - { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, - { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, - { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, ] [[package]] @@ -5587,15 +4741,6 @@ version = "2.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, @@ -5654,8 +4799,7 @@ dependencies = [ { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "networkx" }, { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, @@ -5666,14 +4810,6 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, - { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, - { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, @@ -5701,20 +4837,11 @@ name = "torchvision" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "pillow" }, { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, - { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, - { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, @@ -5756,8 +4883,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, @@ -5777,20 +4903,6 @@ 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/e2/d4/f7ffb855cb039b7568aba4911fbe42e4c39c0e4398387c8e0d8251489992/tree_sitter-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72a510931c3c25f134aac2daf4eb4feca99ffe37a35896d7150e50ac3eee06c7", size = 146749, upload-time = "2025-09-25T17:37:16.475Z" }, - { url = "https://files.pythonhosted.org/packages/9a/58/f8a107f9f89700c0ab2930f1315e63bdedccbb5fd1b10fcbc5ebadd54ac8/tree_sitter-0.25.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44488e0e78146f87baaa009736886516779253d6d6bac3ef636ede72bc6a8234", size = 137766, upload-time = "2025-09-25T17:37:18.138Z" }, - { url = "https://files.pythonhosted.org/packages/19/fb/357158d39f01699faea466e8fd5a849f5a30252c68414bddc20357a9ac79/tree_sitter-0.25.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2f8e7d6b2f8489d4a9885e3adcaef4bc5ff0a275acd990f120e29c4ab3395c5", size = 599809, upload-time = "2025-09-25T17:37:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a4/68ae301626f2393a62119481cb660eb93504a524fc741a6f1528a4568cf6/tree_sitter-0.25.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b570690f87f1da424cd690e51cc56728d21d63f4abd4b326d382a30353acc7", size = 627676, upload-time = "2025-09-25T17:37:20.715Z" }, - { url = "https://files.pythonhosted.org/packages/69/fe/4c1bef37db5ca8b17ca0b3070f2dff509468a50b3af18f17665adcab42b9/tree_sitter-0.25.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0ec41b895da717bc218a42a3a7a0bfcfe9a213d7afaa4255353901e0e21f696", size = 624281, upload-time = "2025-09-25T17:37:21.823Z" }, - { url = "https://files.pythonhosted.org/packages/d4/30/3283cb7fa251cae2a0bf8661658021a789810db3ab1b0569482d4a3671fd/tree_sitter-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:7712335855b2307a21ae86efe949c76be36c6068d76df34faa27ce9ee40ff444", size = 127295, upload-time = "2025-09-25T17:37:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/88/90/ceb05e6de281aebe82b68662890619580d4ffe09283ebd2ceabcf5df7b4a/tree_sitter-0.25.2-cp310-cp310-win_arm64.whl", hash = "sha256:a925364eb7fbb9cdce55a9868f7525a1905af512a559303bd54ef468fd88cb37", size = 113991, upload-time = "2025-09-25T17:37:23.854Z" }, - { 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" }, @@ -5882,10 +4994,6 @@ name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, - { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, - { 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/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, { 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/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, @@ -5933,7 +5041,6 @@ dependencies = [ { name = "botocore-stubs" }, { name = "types-aiobotocore" }, { name = "types-s3transfer" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/76/e162ea2ef8d414d4f36f28a6e0b6078ccef3f2f9d5f957859f303995c528/types_aioboto3-15.5.0.tar.gz", hash = "sha256:5769a1c3df7ca1abedf3656ddf0b970c9b0436d0f88cf4686040b55cd2a02925", size = 81059, upload-time = "2025-10-31T01:11:54.445Z" } wheels = [ @@ -5954,7 +5061,6 @@ version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/e4/5dd83c879eff726931ef222d3ae27f67999c3bc33dfae1ca8237ebccdf17/types_aiobotocore-3.6.0.tar.gz", hash = "sha256:e990056ce9d19094a9d9b36008ca0b2a4d69948ff58f50868dd159a81f6ad69c", size = 87978, upload-time = "2026-05-02T03:05:30.694Z" } wheels = [ @@ -5965,9 +5071,6 @@ wheels = [ name = "types-aiobotocore-bedrock" version = "2.25.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/69/99/b986a2dcd19828b7d79d237d9c6154327bfcc4fd6126b67ea6343322a8b5/types_aiobotocore_bedrock-2.25.2.tar.gz", hash = "sha256:524043f7cf95d4670706b05d48835d8ad3e401fa4095000d107ca444ea3c4e7f", size = 59660, upload-time = "2025-11-12T01:42:00.672Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d3/1d32ce456037008fa0878832f80347d029887a7ea20922745f003def4ab9/types_aiobotocore_bedrock-2.25.2-py3-none-any.whl", hash = "sha256:d06ebd7a8e8ffe22567719136bd73b9a804f77bd83ec7ad02a8077438c5436d3", size = 67366, upload-time = "2025-11-12T01:41:59.369Z" }, @@ -5977,9 +5080,6 @@ wheels = [ name = "types-aiobotocore-bedrock-runtime" version = "2.25.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/54/af/47f695dd846b3898feb0a858020ac86f088c65826d0d60f9680a1177837d/types_aiobotocore_bedrock_runtime-2.25.2.tar.gz", hash = "sha256:7068ac2f4cda13852739db2dc822ef7498c6fe81fc031b496dc35bdcb7cc44f3", size = 28451, upload-time = "2025-11-12T01:42:02.961Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/ba/baeb40c98b0a2be60b5e6c704ed16646f6981618a543087832877aa7b170/types_aiobotocore_bedrock_runtime-2.25.2-py3-none-any.whl", hash = "sha256:8a2e881e0922c6866b5fac1835abfa8b623a2046db9125ea20e2832248ed55a0", size = 34804, upload-time = "2025-11-12T01:42:01.759Z" }, @@ -6017,8 +5117,7 @@ name = "types-networkx" version = "3.6.1.20260408" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/06/fe448df6a42cc938d36fa6e88cb0c6a269059cd4ce05347ec52da306e007/types_networkx-3.6.1.20260408.tar.gz", hash = "sha256:63f94902b2d99d6d4f448eb1bc23b8a327d4022f5098bc0d22d42b13003b106e", size = 73828, upload-time = "2026-04-08T04:34:38.471Z" } wheels = [ @@ -6118,20 +5217,12 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -6150,24 +5241,6 @@ 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/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, @@ -6204,11 +5277,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] @@ -6230,26 +5298,6 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, @@ -6322,42 +5370,6 @@ dependencies = [ ] 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/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, - { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, - { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, - { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, - { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, - { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, - { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, - { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, - { 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" }, From 539726b70b70f0578e3396e479403f7d69430e81 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 00:31:50 +0200 Subject: [PATCH 02/16] Update Mistral Workflows integration and dependencies - Updated `mistralai` dependency to version 2.4.4 in `pyproject.toml`. - Added a new `TODOS.md` file for planning and progress tracking of Mistral Workflows integration. - Introduced new activities and bridge for running Pipelex pipes within Mistral Workflows. - Implemented execution modes for Pipelex pipes in Mistral Workflows. - Added exception handling specific to Mistral Workflows. - Updated various imports to align with the new `mistralai.client` structure. - Enhanced type checking and error handling in workflow scripts. - Added integration tests for the new Mistral Workflows activities. --- .claude/settings.json | 5 +- .../skills/workflows/scripts/test_workflow.py | 116 +-- TODOS.md | 730 ++++++++++++++++++ pipelex/plugins/mistral/mistral_config.py | 2 +- .../plugins/mistral/mistral_extract_worker.py | 3 +- pipelex/plugins/mistral/mistral_factory.py | 17 +- pipelex/plugins/mistral/mistral_llm_worker.py | 17 +- pipelex/plugins/mistral/mistral_llms.py | 7 +- .../plugins/mistralai_workflows/__init__.py | 0 .../plugins/mistralai_workflows/activities.py | 40 + .../plugins/mistralai_workflows/bootstrap.py | 39 + pipelex/plugins/mistralai_workflows/bridge.py | 313 ++++++++ .../plugins/mistralai_workflows/exceptions.py | 17 + .../mistralai_workflows/execution_mode.py | 37 + pyproject.toml | 20 +- .../plugins/mistralai_workflows/conftest.py | 35 + .../test_activities_direct.py | 112 +++ .../mistralai_workflows/test_bridge_direct.py | 85 ++ .../test_data/bridge_funcs.py | 14 + .../test_data/bridge_test.mthds | 8 + .../plugins/mistral/test_mistral_reasoning.py | 2 +- .../test_mistral_worker_error_handling.py | 2 +- .../mistralai_workflows/test_dispatch.py | 121 +++ .../test_execution_mode.py | 18 + .../mistralai_workflows/test_input_models.py | 69 ++ .../mistralai_workflows/test_validation.py | 75 ++ .../test_plugin_pipelex_storage_images.py | 2 +- uv.lock | 194 ++--- 28 files changed, 1902 insertions(+), 198 deletions(-) create mode 100644 TODOS.md create mode 100644 pipelex/plugins/mistralai_workflows/__init__.py create mode 100644 pipelex/plugins/mistralai_workflows/activities.py create mode 100644 pipelex/plugins/mistralai_workflows/bootstrap.py create mode 100644 pipelex/plugins/mistralai_workflows/bridge.py create mode 100644 pipelex/plugins/mistralai_workflows/exceptions.py create mode 100644 pipelex/plugins/mistralai_workflows/execution_mode.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/conftest.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds create mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py create mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py create mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py create mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py diff --git a/.claude/settings.json b/.claude/settings.json index 8eb165fe0..c65924b97 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -41,5 +41,8 @@ ] } ] + }, + "enabledPlugins": { + "temporal@temporal-marketplace": true } -} \ No newline at end of file +} diff --git a/.claude/skills/workflows/scripts/test_workflow.py b/.claude/skills/workflows/scripts/test_workflow.py index 45d1cd6bc..ad3e99ec3 100644 --- a/.claude/skills/workflows/scripts/test_workflow.py +++ b/.claude/skills/workflows/scripts/test_workflow.py @@ -19,7 +19,6 @@ import importlib.util import inspect import json -import os import sys import traceback from pathlib import Path @@ -66,11 +65,7 @@ def _import_module(file_path: Path) -> Any: def _find_workflow_classes(module: Any) -> list[type]: """Return all @workflow.define classes in *module*.""" - return [ - obj - for _, obj in inspect.getmembers(module, inspect.isclass) - if hasattr(obj, "__workflows_workflow_def") - ] + return [obj for _, obj in inspect.getmembers(module, inspect.isclass) if hasattr(obj, "__workflows_workflow_def")] def _workflow_name(cls: type) -> str: @@ -81,33 +76,25 @@ def _is_interactive(cls: type) -> bool: return issubclass(cls, workflows.InteractiveWorkflow) -def _discover_workflow( - workflow_file: Path, name_override: str | None -) -> tuple[type, str, bool]: +def _discover_workflow(workflow_file: Path, name_override: str | None) -> tuple[type, str, bool]: """Find and select the workflow class. Returns (cls, name, interactive).""" module = _import_module(workflow_file) found = _find_workflow_classes(module) if not found: - raise SystemExit( - f"No workflow classes found in {workflow_file}. " - "Ensure the file has a class decorated with @workflow.define." - ) + raise SystemExit(f"No workflow classes found in {workflow_file}. Ensure the file has a class decorated with @workflow.define.") if name_override: matches = [w for w in found if _workflow_name(w) == name_override] if not matches: available = ", ".join(_workflow_name(w) for w in found) - raise SystemExit( - f"Workflow '{name_override}' not found. Available: {available}" - ) + raise SystemExit(f"Workflow '{name_override}' not found. Available: {available}") cls = matches[0] else: if len(found) > 1: names = ", ".join(_workflow_name(w) for w in found) print( - f"Multiple workflows found: {names}. Using the first one. " - "Pass --workflow-name to select.", + f"Multiple workflows found: {names}. Using the first one. Pass --workflow-name to select.", file=sys.stderr, ) cls = found[0] @@ -146,9 +133,7 @@ class _Payload(BaseModel): input: dict for i, response_data in enumerate(interactions, 1): - task_id = await _wait_for_pending_input( - client, execution_id, i, poll_timeout, poll_interval - ) + task_id = await _wait_for_pending_input(client, execution_id, i, poll_timeout, poll_interval) print(f" Interaction {i}: submitting {json.dumps(response_data)}") try: @@ -161,10 +146,9 @@ class _Payload(BaseModel): ), timeout=30, ) - except asyncio.TimeoutError: + except TimeoutError: print( - f" Interaction {i}: update timed out " - "(workflow may have failed to process the input)", + f" Interaction {i}: update timed out (workflow may have failed to process the input)", file=sys.stderr, ) raise @@ -186,25 +170,18 @@ async def _wait_for_pending_input( start = asyncio.get_event_loop().time() while True: try: - resp = await client.workflows.executions.query_workflow_execution_async( - execution_id=execution_id, name="__get_pending_inputs" - ) + resp = await client.workflows.executions.query_workflow_execution_async(execution_id=execution_id, name="__get_pending_inputs") pending = resp.result.get("pending_inputs", []) if pending: task_id = pending[0]["task_id"] label = pending[0].get("label", "") - print( - f" Interaction {index}: pending input found " - f"(task={task_id[:8]}..., label={label!r})" - ) + print(f" Interaction {index}: pending input found (task={task_id[:8]}..., label={label!r})") return task_id except Exception: pass if asyncio.get_event_loop().time() - start > timeout: - raise TimeoutError( - f"Timeout waiting for pending input #{index} ({timeout}s)" - ) + raise TimeoutError(f"Timeout waiting for pending input #{index} ({timeout}s)") await asyncio.sleep(interval) @@ -213,9 +190,7 @@ async def _wait_for_pending_input( # --------------------------------------------------------------------------- -async def _execute_with_retry( - client: Any, wf_name: str, input_dict: dict | None, retries: int = 10 -) -> Any: +async def _execute_with_retry(client: Any, wf_name: str, input_dict: dict | None, retries: int = 10) -> Any: """Start the workflow, retrying on registration-propagation errors.""" for attempt in range(retries): try: @@ -244,24 +219,14 @@ async def _await_result( """ if not (interactive and interactions): final = await asyncio.wait_for( - client.workflows.wait_for_workflow_completion_async( - execution_id, polling_interval=2 - ), + client.workflows.wait_for_workflow_completion_async(execution_id, polling_interval=2), timeout=timeout, ) return final.result # Run interactions and completion polling concurrently. - interaction_task = asyncio.create_task( - _poll_and_submit_interactions( - client, execution_id, interactions, poll_timeout=timeout - ) - ) - completion_task = asyncio.create_task( - client.workflows.wait_for_workflow_completion_async( - execution_id, polling_interval=2 - ) - ) + interaction_task = asyncio.create_task(_poll_and_submit_interactions(client, execution_id, interactions, poll_timeout=timeout)) + completion_task = asyncio.create_task(client.workflows.wait_for_workflow_completion_async(execution_id, polling_interval=2)) done, pending = await asyncio.wait( [interaction_task, completion_task], @@ -277,7 +242,7 @@ async def _await_result( pass if not done: - raise asyncio.TimeoutError() + raise TimeoutError # Surface errors -- interaction errors take priority. if interaction_task in done and interaction_task.exception(): @@ -288,7 +253,7 @@ async def _await_result( if completion_task in done: return completion_task.result().result - raise asyncio.TimeoutError() + raise TimeoutError # --------------------------------------------------------------------------- @@ -312,9 +277,7 @@ async def run_workflow( _ensure_sdk() # -- discover -- - workflow_cls, wf_name, interactive = _discover_workflow( - workflow_file, workflow_name_override - ) + workflow_cls, wf_name, interactive = _discover_workflow(workflow_file, workflow_name_override) print(f"Workflow: {wf_name} ({workflow_cls.__name__})") print(f"Interactive: {interactive}") @@ -326,8 +289,7 @@ async def run_workflow( if interactive and not interactions: print( - "WARNING: Interactive workflow but no --interactions provided.\n" - " The workflow will hang at wait_for_input().\n", + "WARNING: Interactive workflow but no --interactions provided.\n The workflow will hang at wait_for_input().\n", file=sys.stderr, ) @@ -348,25 +310,19 @@ async def run_workflow( execution_id: str | None = None try: # -- execute -- - execution = await _execute_with_retry( - client, wf_name, _build_input(input_data) - ) + execution = await _execute_with_retry(client, wf_name, _build_input(input_data)) execution_id = execution.execution_id print(f"Execution: {execution_id}") print(f"Status: {execution.status}\n") # -- wait for result -- - return await _await_result( - client, execution_id, interactions, interactive, timeout_seconds - ) + return await _await_result(client, execution_id, interactions, interactive, timeout_seconds) - except asyncio.TimeoutError: + except TimeoutError: if execution_id: print(f"\nTerminating execution {execution_id}...", file=sys.stderr) try: - await client.workflows.executions.terminate_workflow_execution_async( - execution_id=execution_id - ) + await client.workflows.executions.terminate_workflow_execution_async(execution_id=execution_id) print("Execution terminated.", file=sys.stderr) except Exception as e: print(f"Failed to terminate: {e}", file=sys.stderr) @@ -393,27 +349,32 @@ def _parse_args() -> argparse.Namespace: epilog=__doc__, ) parser.add_argument( - "workflow_file", type=Path, + "workflow_file", + type=Path, help="Path to the Python file containing the workflow.", ) parser.add_argument( - "--input", required=True, dest="input_json", + "--input", + required=True, + dest="input_json", help="JSON string with the workflow input.", ) parser.add_argument( - "--timeout", type=int, default=30, + "--timeout", + type=int, + default=30, help="Max seconds before the workflow is killed (default: 30).", ) parser.add_argument( - "--workflow-name", default=None, + "--workflow-name", + default=None, help="Workflow name (if the file contains multiple workflows).", ) parser.add_argument( - "--interactions", default=None, dest="interactions_json", - help=( - "JSON array of interaction responses for interactive workflows. " - 'Example: \'[{"choice": "WFL"}]\'' - ), + "--interactions", + default=None, + dest="interactions_json", + help=('JSON array of interaction responses for interactive workflows. Example: \'[{"choice": "WFL"}]\''), ) return parser.parse_args() @@ -445,6 +406,7 @@ def main() -> None: # Load .env if present. try: from dotenv import load_dotenv + load_dotenv(override=True) except ImportError: pass @@ -468,7 +430,7 @@ def main() -> None: print(json.dumps(result, indent=2, default=str)) except SystemExit: raise - except asyncio.TimeoutError: + except TimeoutError: print("FAILED: workflow timed out", file=sys.stderr) raise SystemExit(1) except Exception: diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 000000000..e8dcf0656 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,730 @@ +# Mistral Workflows ↔ Pipelex Plugin — Plan & Progress + +Self-contained planning document. A fresh session can resume from this file +alone; no need to read prior conversation history. + +--- + +## 1. Context + +We are integrating Pipelex with Mistral Workflows +(`mistralai-workflows>=3.3.0`), the Mistral orchestration framework that wraps +Temporal with a thicker DX layer. Two goals were considered: + +- **Goal 1** — Port Pipelex orchestration to run on Mistral Workflows as an + alternative durable runtime (replacing/duplicating our existing Temporal + integration). Outcome: feasible but high friction (3–4 weeks). **Deferred** + pending answers from the Mistral team about extension hooks for payload + converter, codec, sandbox, and run_worker. Out of scope for this plan. +- **Goal 2** — Let users invoke Pipelex pipes from inside their own Mistral + Workflows activities. Low risk, clear user value, ~2 weeks. **In scope.** + +This plan covers Goal 2 only. The `mistralai-workflows` dependency must remain +strictly optional. + +--- + +## 2. Background — what we know about Mistral Workflows + +Verified by reading the installed package +(`/Users/lchoquel/repos/Pipelex/_mistral/.venv/lib/python3.13/site-packages/mistralai/workflows/`): + +- Mistral Workflows IS Temporal underneath. Their `@workflow.define` decorator + ultimately calls `temporalio.workflow.defn(sandboxed=...)` (see + `mistralai/workflows/core/workflow.py:170, 251`). +- Their decorator wraps the user's `run` method so the workflow's runtime + signature becomes `run(self, params: dict | None)`. Caller side dumps params + via `params.model_dump()` (`core/execution/workflow_execution.py:192-204`). + This breaks any kajson-based subclass preservation at the workflow boundary + but **does not affect activities**, whose payload converter handles arg + serialization directly. +- The Mistral worker hardcodes `MistralWorkflowsPayloadConverter` and + `MistralWorkflowsPayloadCodec` (`core/worker.py:421-425`). No public override. +- `run_worker(workflows)` connects to the Mistral cloud control plane, + registers schemas, heartbeats. Requires `MISTRAL_API_KEY`. There is a + `mistralai.workflows.testing.create_test_worker` for in-process Temporal + test envs that does NOT require the cloud. +- Activities (`@workflows.activity`) are unrestricted Python — no sandbox, + no schema constraints beyond JSON-serializable types, no cloud dep at + invocation time. +- There is a "local execution" mode where `execute_workflow` runs the entry + method directly without Temporal — only useful for prototyping. +- `OffloadableField` (from `mistralai.extra.workflows`) + an + `ActivityInOutOffloadingInterceptor` provide automatic large-payload + offloading at the activity boundary. + +Pipelex side (verified by reading the repo): + +- `PipeJob` (`pipelex/pipe_run/pipe_job.py:13`) is a BaseModel that already + has `prepare_for_temporal()` — dehydrates `WorkingMemory` to a raw dict + when a `LibraryCrate` is present. +- Direct-mode execution: `PipeRun(pipe_router=...)` in + `pipelex/pipe_run/pipe_run.py:21`. May need a `make_direct_pipe_run()` + factory if absent — confirm in pre-flight. +- Temporal-mode execution: `make_temporal_pipe_run(...)` in + `pipelex/temporal/tprl_pipe/temporal_pipe_run.py:104`. Provides `.run()` + (blocking) and `.start()` (returns `(workflow_id, handle)`). +- `LibraryCrate` already round-trips through Pipelex's own Temporal codec — + same `model_dump`/`model_validate` will work for our boundary. +- Existing precedent for plugin layout: `pipelex/plugins/mistral/` (the + Mistral inference plugin). We follow the same shape. +- `pyproject.toml` already declares + `mistralai-workflows = ["mistralai-workflows>=3.3.0"]` in + `[project.optional-dependencies]`. No change needed there for Phase 1. + +--- + +## 3. Locked-in design decisions + +- [x] **Library crate transport**: dump-based. + `PipelexPipeRunInput.library_crate_dump: dict[str, Any] | None`. No + registry-based variant in Phase 1; can be layered in later if asked. +- [x] **Execution mode**: an exhaustive `StrEnum`, set per-call (not + per-worker). Three modes: `DIRECT`, `TEMPORAL_BLOCKING`, + `TEMPORAL_FIRE_AND_FORGET`. +- [x] **Streaming**: in-scope, phased — Phase 2.0 ships a single Mistral + `task()` per activity (started/completed/failed); Phase 2.1 ships + per-step `task.update()` events driven by Pipelex's `report_delegate`, + conditional on demand. +- [x] **Plugin location**: `pipelex/plugins/mistralai_workflows/`. +- [x] **Optional-dep guard**: guard lives in `activities.py` (and will live in + `streaming.py` when added). `__init__.py` is empty per Pipelex's + "no re-exports" rule. `bridge.py`, `execution_mode.py`, `bootstrap.py`, + `exceptions.py` are framework-agnostic and importable on a venv that + does NOT have `mistralai-workflows` installed. +- [x] **Boundary types are JSON-only**. `inputs` and `output_dict` are + `dict[str, Any]`; `library_crate_dump` is a dict. No Pipelex internal + types (PipeJob, PipeOutput, WorkingMemory) cross the activity boundary. + +--- + +## 4. Module layout (final) + +``` +pipelex/plugins/mistralai_workflows/ +├── __init__.py # optional-dep guard ONLY (no re-exports) +├── exceptions.py # 4 exception classes +├── execution_mode.py # PipelexExecutionMode StrEnum +├── bridge.py # framework-agnostic core (NO mistralai import) +├── bootstrap.py # ensure_pipelex_booted() + DI helper +├── activities.py # @activity-decorated wrappers +└── streaming.py # Phase 2: mistral task() event forwarding + +tests/integration/pipelex/plugins/mistralai_workflows/ +├── conftest.py # mistralai = pytest.importorskip(...) +├── test_bridge.py # layer 1: no optional dep needed +├── test_activities_direct.py # layer 2: needs mistralai-workflows +├── test_activities_temporal_blocking.py # layer 3: + temporal extra +└── test_activities_streaming.py # Phase 2 +``` + +Per Pipelex's "no re-exports in `__init__.py`" rule, users import from full +paths: + +```python +from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +``` + +The `__init__.py` exists only to host the import-time guard. + +--- + +## 5. Public API — three usage tiers + +### Tier 1 — pre-decorated activity + +```python +from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + +@workflows.workflow.define(name="my-flow") +class MyFlow: + @workflows.workflow.entrypoint + async def run(self, doc_url: str) -> dict: + result = await pipelex_run_pipe(PipelexPipeRunInput( + pipe_code="extract_invoice", + inputs={"doc_url": doc_url}, + execution_mode=PipelexExecutionMode.DIRECT, + )) + return result.output_dict +``` + +### Tier 2 — bridge helper inside user's own typed activity + +```python +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, run_pipe_via_bridge, +) + +@workflows.activity(start_to_close_timeout=timedelta(minutes=30), rate_limit=quota) +async def extract_invoice(doc_url: str) -> InvoiceData: + out = await run_pipe_via_bridge(PipelexPipeRunInput( + pipe_code="extract_invoice", inputs={"doc_url": doc_url}, + )) + return InvoiceData.model_validate(out.output_dict) +``` + +`run_pipe_via_bridge` is the same code the Tier-1 activity calls — just +without the `@activity` decoration. Lets users own per-pipe activity +configuration (timeouts, rate limits, sticky-to-worker, names). + +### Tier 3 — full control + +Use `build_pipe_job_from_input(...)` and `serialize_pipe_output(...)` from +`bridge.py` directly. For multi-pipe activities, custom delivery, or test +fixtures. + +--- + +## 6. Concrete designs — file by file + +### `execution_mode.py` + +```python +from pipelex.types import StrEnum + +class PipelexExecutionMode(StrEnum): + """How a Pipelex pipe runs inside a Mistral Workflows activity. + + DIRECT: in-process; no Temporal involved on Pipelex's side; activity + blocks until the pipe completes. Fastest feedback, simplest ops. + TEMPORAL_BLOCKING: dispatch the pipe as a Pipelex Temporal workflow; + the activity awaits completion. Pipe runs durably on Pipelex's + worker fleet. Requires pipelex[temporal] extra. + TEMPORAL_FIRE_AND_FORGET: dispatch the pipe as a Pipelex Temporal + workflow and return immediately with the workflow_id. Activity + does NOT wait; completion is signalled out-of-band via + DeliveryAssignment (webhook / storage). Same dep requirements + as TEMPORAL_BLOCKING. delivery_assignment_dump is required. + """ + + DIRECT = "direct" + TEMPORAL_BLOCKING = "temporal_blocking" + TEMPORAL_FIRE_AND_FORGET = "temporal_fire_and_forget" + + @property + def requires_pipelex_temporal(self) -> bool: + match self: + case PipelexExecutionMode.DIRECT: + return False + case ( + PipelexExecutionMode.TEMPORAL_BLOCKING + | PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET + ): + return True +``` + +Exhaustive `match` (no `case _:`) — Pipelex linting requires this and we get +linter errors when a new mode is added. + +### `exceptions.py` + +```python +from pipelex.exceptions import PipelexError + +class MistralWorkflowsPluginError(PipelexError): + pass + +class MistralWorkflowsNotInstalledError(MistralWorkflowsPluginError, ImportError): + pass + +class MissingPipelexTemporalExtraError(MistralWorkflowsPluginError): + pass + +class PipelexBridgeRuntimeError(MistralWorkflowsPluginError): + pass +``` + +### `bootstrap.py` + +```python +from pathlib import Path +from typing import Callable +from pipelex import Pipelex +from pipelex.system.runtime import RunMode + +_BOOTED = False + +def ensure_pipelex_booted( + config_dir: Path | None = None, + force_run_mode: RunMode | None = None, +) -> None: + """Idempotent. Boots Pipelex on first call; no-op afterwards.""" + global _BOOTED + if _BOOTED: + return + Pipelex.make(config_dir=config_dir, run_mode=force_run_mode) + _BOOTED = True + +def get_pipelex_dependency() -> Callable[[], Pipelex]: + """Returns a callable suitable for mistralai.workflows Depends(...).""" + def _resolver() -> Pipelex: + ensure_pipelex_booted() + return Pipelex.get_instance() + return _resolver +``` + +User's worker entry-point: + +```python +async def main() -> None: + ensure_pipelex_booted() # boot once before workers start + await workflows.run_worker([MyFlow], activities=[pipelex_run_pipe]) +``` + +We don't auto-magic the boot from inside the activity for production +correctness — users should know Pipelex is running. The defensive call +inside `run_pipe_via_bridge` exists only to make first-time-runner mistakes +survivable. + +### `bridge.py` + +NO `mistralai.workflows` or `temporalio` imports at module top-level. +The Temporal extra is lazy-imported inside the temporal-mode branches. + +```python +from typing import Any +from pydantic import BaseModel, ConfigDict, Field +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted +from pipelex.plugins.mistralai_workflows.exceptions import ( + PipelexBridgeRuntimeError, MissingPipelexTemporalExtraError, +) + + +class PipelexPipeRunInput(BaseModel): + """JSON-safe input crossing the Mistral/Temporal boundary.""" + model_config = ConfigDict(extra="forbid") + + pipe_code: str + domain_code: str | None = None + inputs: dict[str, Any] = Field(default_factory=dict) + output_name: str | None = None + pipeline_run_id: str | None = None # generated if None + user_id: str | None = None + library_crate_dump: dict[str, Any] | None = None + execution_mode: PipelexExecutionMode = PipelexExecutionMode.DIRECT + delivery_assignment_dump: dict[str, Any] | None = None + + +class PipelexPipeRunOutput(BaseModel): + """JSON-safe output crossing the Mistral/Temporal boundary.""" + model_config = ConfigDict(extra="forbid") + + output_dict: dict[str, Any] + main_stuff_name: str | None = None + pipeline_run_id: str + workflow_id: str | None = None # set when execution_mode is TEMPORAL_* + is_completed: bool # False for FIRE_AND_FORGET + graph_spec_dump: dict[str, Any] | None = None + + +def build_pipe_job_from_input(input: PipelexPipeRunInput) -> "PipeJob": + """Hydrate a PipeJob from JSON-safe input. Loads library_crate if given.""" + ... + +def serialize_pipe_output(output: "PipeOutput") -> dict[str, Any]: + """Dehydrate PipeOutput to JSON-safe dict via dump_for_json/temporal.""" + ... + + +async def run_pipe_via_bridge(input: PipelexPipeRunInput) -> PipelexPipeRunOutput: + ensure_pipelex_booted() + _validate_input(input) + pipe_job = build_pipe_job_from_input(input) + delivery = _build_delivery_assignment(input.delivery_assignment_dump) + + match input.execution_mode: + case PipelexExecutionMode.DIRECT: + return await _run_direct(pipe_job, delivery) + case PipelexExecutionMode.TEMPORAL_BLOCKING: + _require_pipelex_temporal_extra() + return await _run_temporal_blocking(pipe_job, delivery) + case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + _require_pipelex_temporal_extra() + return await _run_temporal_fire_and_forget(pipe_job, delivery) + + +def _validate_input(input: PipelexPipeRunInput) -> None: + if ( + input.execution_mode is PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET + and input.delivery_assignment_dump is None + ): + msg = ( + "TEMPORAL_FIRE_AND_FORGET requires a delivery_assignment_dump; " + "otherwise the pipe completion is silently dropped." + ) + raise PipelexBridgeRuntimeError(msg) + + +def _require_pipelex_temporal_extra() -> None: + try: + import temporalio # noqa: F401, PLC0415 + except ImportError as exc: + msg = ( + "TEMPORAL_* execution modes require the pipelex[temporal] extra. " + "Install with: pip install 'pipelex[temporal,mistralai-workflows]'" + ) + raise MissingPipelexTemporalExtraError(msg) from exc +``` + +`_run_direct`, `_run_temporal_blocking`, `_run_temporal_fire_and_forget` are +private helpers; they wrap pipe-run failures into +`PipelexBridgeRuntimeError` (chained from the original exception). No +`except Exception` per Pipelex standards — only catch +`PipeRunError`/`PipeJobError` (and `WorkflowExecutionError` for temporal +modes) explicitly. + +### `__init__.py` + +```python +from pipelex.plugins.mistralai_workflows.exceptions import ( + MistralWorkflowsNotInstalledError, +) + +try: + import mistralai.workflows # noqa: F401 +except ImportError as exc: + msg = ( + "The 'mistralai-workflows' optional dependency is not installed. " + "Install with: pip install 'pipelex[mistralai-workflows]'" + ) + raise MistralWorkflowsNotInstalledError(msg) from exc +``` + +Note: `bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py` can +be imported even when `mistralai-workflows` is NOT installed, because the +guard is in `__init__.py` — but only triggers when the package itself is +imported. To preserve this, **users importing the framework-agnostic +modules must import them via `pipelex.plugins.mistralai_workflows.bridge` +etc., which will run the guard first.** This means the optional dep IS +required even for Tier-3 use. If we want the framework-agnostic core to be +usable without the optional dep, move the guard out of `__init__.py` and +into `activities.py` + `streaming.py` only. + +**Decision pending in pre-flight**: should `bridge.py` / `execution_mode.py` +be importable without `mistralai-workflows`? Recommended **yes**, so move +the guard into `activities.py` and `streaming.py` only. (Updated +recommendation overrides §4 / §8 of original design draft.) + +### `activities.py` + +```python +from datetime import timedelta +from mistralai.workflows import activity # this triggers ImportError if missing + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge, +) + + +@activity( + start_to_close_timeout=timedelta(minutes=10), + retry_policy_max_attempts=3, +) +async def pipelex_run_pipe(input: PipelexPipeRunInput) -> PipelexPipeRunOutput: + return await run_pipe_via_bridge(input) +``` + +Future: an `OffloadableField`-using variant for large payloads (Phase 1.5). + +--- + +## 7. Pre-flight verification (before Phase 1.0) + +- [x] `PipeOutput` exposes `prepare_for_temporal(library_crate)` that delegates + to `WorkingMemory.dump_for_temporal()`. We don't need a new + `dump_for_json()`. The bridge serializes via + `working_memory.dump_for_temporal()` directly so the output shape is + consistent regardless of `library_crate`. +- [x] No `make_direct_pipe_run()` factory needed. `PipeRouter()` has no + required args; the bridge constructs `PipeRun(pipe_router=PipeRouter())` + inline inside `_run_direct`. +- [x] `LibraryCrate.model_dump()` / `model_validate()` round-trip cleanly + (verified by `test_bridge_direct.test_direct_mode_with_library_crate_dump`). +- [x] `Pipelex.make()` is **NOT** idempotent — it raises if a singleton + already exists. `bootstrap.ensure_pipelex_booted()` calls + `Pipelex.make()` only when `Pipelex.get_optional_instance() is None`, + which adopts an externally-booted singleton without re-initializing. +- [x] **Guard placement**: chose to put the optional-dep guard in + `activities.py` only (and in `streaming.py` when added). `__init__.py` + is empty. `bridge.py`, `execution_mode.py`, `bootstrap.py`, and + `exceptions.py` are importable without `mistralai-workflows`. +- [x] `DeliveryAssignment.model_dump()` / `model_validate()` round-trip + cleanly (plain BaseModel; verified in `test_validation`). + +--- + +## 8. Phasing & checklist + +### Phase 1.0 — Framework-agnostic core (no optional dep) — **DONE** + +Files: `bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`. +None of these import `mistralai.workflows` at module top-level. `bridge.py` +lazy-imports `temporalio` only inside the temporal-mode branches. + +- [x] Create `pipelex/plugins/mistralai_workflows/` package directory +- [x] Create `__init__.py` (empty — guard lives in `activities.py` only) +- [x] Create `exceptions.py` with all 4 exception classes + (`MistralWorkflowsPluginError`, `MistralWorkflowsNotInstalledError`, + `MissingPipelexTemporalExtraError`, `PipelexBridgeRuntimeError`) +- [x] Create `execution_mode.py` with `PipelexExecutionMode` StrEnum and + `requires_pipelex_temporal` + `is_fire_and_forget` properties using + exhaustive `match/case` +- [x] Create `bootstrap.py` with `ensure_pipelex_booted()` (idempotent via + `Pipelex.get_optional_instance()` singleton check — no module-level + flag needed) and `get_pipelex_dependency()` factory +- [x] Create `bridge.py`: + - [x] `PipelexPipeRunInput` BaseModel (`extra="forbid"`) + - [x] `PipelexPipeRunOutput` BaseModel (`extra="forbid"`) + - [x] `build_pipe_job_from_input(input) -> PipeJob` + - [x] `serialize_pipe_output(pipe_output) -> dict[str, Any]` — + always uses `WorkingMemory.dump_for_temporal()` for stable shape + - [x] `run_pipe_via_bridge(input) -> PipelexPipeRunOutput` with + exhaustive mode dispatch + - [x] `_run_direct`, `_run_temporal_blocking`, + `_run_temporal_fire_and_forget` private helpers + - [x] `_require_pipelex_temporal_extra()` lazy-import guard + - [x] `_validate_input()`: FIRE_AND_FORGET + no delivery → raise + - [x] `_scoped_library_for_crate()` async context manager for per-call + scoped library when a `library_crate_dump` is provided +- [x] Layer-1 tests — **split across modules** (1 TestClass per module per + Pipelex pytest standards): + - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py` + — input/output BaseModel validation (forbid extra, required + fields, defaults, JSON round-trip) + - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py` + — `PipelexExecutionMode` properties + - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py` + — `_validate_input` (FIRE_AND_FORGET requires delivery), + `_decode_library_crate` / `_decode_delivery_assignment` + round-trips, `run_pipe_via_bridge` validation error path + - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py` + — DIRECT / TEMPORAL_BLOCKING / TEMPORAL_FIRE_AND_FORGET dispatch + with mocked `PipeRun.run` and `make_temporal_pipe_run` (uses + `PipeJob.model_construct` to bypass Pydantic's pipe validation) + - [x] `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` + — DIRECT-mode end-to-end against a real loaded `PipeFunc` test + pipe; covers globally-loaded library, `library_crate_dump` + round-trip, and caller-supplied `pipeline_run_id` +- [x] `make agent-check` passes (lint, ruff, pyright, mypy) +- [ ] `make agent-check` passes with `mistralai-workflows` NOT installed + (not yet verified — needs a fresh venv without the extra) + +### Phase 1.1 — Tier 1 activity wrapper — **DONE** (modulo optional pytest marker) + +- [x] Create `activities.py` (top-of-file imports `mistralai.workflows` — + raises `MistralWorkflowsNotInstalledError` with install hint if missing) +- [x] `pipelex_run_pipe` — `@activity(start_to_close_timeout=10min, + retry_policy_max_attempts=3)`-decorated wrapper around + `run_pipe_via_bridge` +- [x] Create `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` + with the `bridge_test_library` fixture (loads test pipe + registers + its `mistralai_workflows_bridge_echo` PipeFunc target). The + `pytest.importorskip("mistralai.workflows")` lives at module-level of + `test_activities_direct.py` so the rest of the dir (layer-1 bridge + tests) is NOT skipped when the optional dep is missing. +- [x] Layer-2 integration test (`test_activities_direct.py`): + - [x] Spin `WorkflowEnvironment.start_local` + `create_test_worker` + - [x] Define a test workflow that calls `pipelex_run_pipe` in DIRECT + mode against the registered PipeFunc test pipe + - [x] Assert `output_dict` shape and `is_completed=True` +- [ ] (Optional) Add `mistralai_workflows` pytest marker to + `pyproject.toml` markers — **deferred**: the module-level + `importorskip` already gates the test correctly without a marker. +- [ ] (Optional) Extend `[tool.pytest] addopts` default `-m` filter to + exclude `mistralai_workflows` — **deferred** for the same reason. + +**Issues uncovered + fixed in Phase 1.1**: + +1. *Workflow sandbox* rejected pipelex imports during workflow class + validation. Fixed by wrapping pipelex imports in + `mistralai_workflows.workflow.unsafe.imports_passed_through()` AND + passing `enforce_determinism=False` to `@workflow.define` for the test + workflow. +2. *Search attribute* — Mistral's `@workflow.define` wrapper upserts an + `OtelTraceId` keyword search attribute on every workflow run. The dev + server rejects the activation if the attribute isn't pre-registered, + so the test passes `search_attributes=[SearchAttributeKey.for_keyword( + "OtelTraceId")]` to `WorkflowEnvironment.start_local`. +3. *Task queue mismatch* — Mistral's `@activity` wrapper dispatches via + `temporalio.workflow.execute_activity(..., task_queue= + config.get_effective_task_queue())`, which reads the **global** + `mistralai_config.temporal.task_queue` (default `"default"`) — NOT the + workflow's task queue. If we don't override it, activities are + scheduled on `"default"` while the test worker polls our test queue, + causing the workflow to hang. Fixed by an autouse module-scoped + fixture in `test_activities_direct.py` that pins + `mistralai_config.temporal.task_queue = TEST_TASK_QUEUE` for the + duration of the module and restores it on teardown. +4. *Result shape* — Mistral's `convert_result_to_temporal_format` wraps + non-BaseModel returns in `{"result": ...}`. Returning + `PipelexPipeRunOutput` (a BaseModel) directly from the workflow's + entrypoint avoids the wrapping — no shape mangling. +5. *mypy* — `mistralai-workflows`'s own source uses PEP 695 type-parameter + syntax that mypy rejects under `python_version=3.11`. Added a + `[[tool.mypy.overrides]]` block in `pyproject.toml` with + `follow_imports = "skip"` and `ignore_errors = true` for + `mistralai.workflows.*`. + +### Phase 1.2 — Temporal modes + +- [ ] Wire `_run_temporal_blocking` to call `make_temporal_pipe_run()` and + await `.run(pipe_job, delivery_assignment)` +- [ ] Wire `_run_temporal_fire_and_forget` to call + `make_temporal_pipe_run().start(...)`, return immediately with + `workflow_id` and `is_completed=False` +- [ ] Layer-3 integration test (`test_activities_temporal_blocking.py`): + - [ ] `pytest.importorskip("temporalio")` at module level + - [ ] Boot Pipelex with `temporal.is_enabled=true` against the test + Temporal env + - [ ] Run a Mistral activity that dispatches a Pipelex `WfPipeRun` + and blocks on the result + - [ ] Assert end-to-end output equality with a DIRECT-mode reference + run of the same pipe +- [ ] Layer-3 fire-and-forget test: + - [ ] Mock `DeliveryExecutor` / webhook target + - [ ] Verify activity returns immediately with non-None + `workflow_id` and `is_completed=False` + - [ ] Verify the Pipelex workflow eventually completes and posts to + the delivery target + +### Phase 1.3 — Docs, changelog, CI matrix + +- [ ] Write `docs/under-the-hood/mistralai-workflows-plugin.md` (overview + + install + when to use which `PipelexExecutionMode`) +- [ ] Write `docs/under-the-hood/mistralai-workflows-recipes.md` with + worked examples: Tier 1, Tier 2, library_crate +- [ ] Update `CHANGELOG.md` Unreleased: "Added: Pipelex pipes can now be + invoked from inside Mistral Workflows activities via the new + `pipelex.plugins.mistralai_workflows` plugin." +- [ ] CI matrix: + - [ ] `unit` lane: `pip install -e .[dev]` — runs layer 1 + - [ ] `mistralai-workflows` lane: + `pip install -e .[dev,mistralai-workflows]` — adds layer 2 + - [ ] `mistralai-workflows-temporal` lane: + `pip install -e .[dev,mistralai-workflows,temporal]` — adds + layer 3 +- [ ] Add a starter example to `pipelex-cookbook/` under a new + `mistral-workflows/` directory + +### Phase 1.5 — Large payload offloading + +- [ ] Add an `OffloadableField`-using variant of `PipelexPipeRunInput` / + `PipelexPipeRunOutput` in `activities.py` (the import lives behind + the optional-dep boundary, fine) +- [ ] Wire the variant into a second pre-decorated activity + `pipelex_run_pipe_offloaded`, OR add a parameter to the existing one +- [ ] Test with a large fixture (>2MB) to confirm offload path works + end-to-end with Mistral's `ActivityInOutOffloadingInterceptor` +- [ ] Document the trade-off (output stored in Mistral-managed storage) + +### Phase 2.0 — Streaming v1 (one task per activity) + +- [ ] Create `streaming.py` (imports `mistralai.workflows`) +- [ ] Wrap `pipelex_run_pipe` body in `async with workflows.task(...) as t:` +- [ ] Emit `started` event with `pipe_code` + `pipeline_run_id` +- [ ] Emit `completed` with output summary on success +- [ ] Emit `failed` with exception details on error +- [ ] Layer-4 streaming test using `create_test_worker_with_events` + + `create_capturing_mock_events_client` + +### Phase 2.1 — Streaming v2 (per-step granularity, conditional on demand) + +- [ ] Subscribe to `report_delegate` event stream from inside the activity +- [ ] Map Pipelex events to Mistral `task.update(...)` calls: + - Pipe sub-step started → `in_progress` with description + - Stuff added to working memory → `in_progress` with new key + - Pipe step completed → progress % +- [ ] Forwarder side-task: drain event log; terminate cleanly when the + activity returns; cover both success and failure paths +- [ ] Test: assert per-step events emitted in correct order for a + multi-step pipe + +--- + +## 9. Pyproject.toml changes — actual + +**Applied**: + +- Added a `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` with + `follow_imports = "skip"` and `ignore_errors = true` (mistralai's source + uses PEP 695 type syntax that mypy rejects under `python_version=3.11`). + +**Deferred** — the module-level `pytest.importorskip("mistralai.workflows")` +in `test_activities_direct.py` already gates the layer-2 test correctly +without a marker. Reconsider if test runtime grows or other tests need to +opt in/out of the optional dep: + +```toml +# tool.pytest markers — not yet added +"mistralai_workflows: tests that require the mistralai-workflows optional dependency", +``` + +```toml +# tool.pytest addopts default exclusion — not yet extended +"-m", "not (inference or llm or img_gen or extract or search or pipelex_api or mistralai_workflows)", +``` + +`[project.optional-dependencies].mistralai-workflows` already declared: +`["mistralai-workflows>=3.3.0"]`. No change. + +--- + +## 10. Phase 1 done criteria + +- [ ] Plugin module compiles and `make agent-check` passes with + `mistralai-workflows` installed +- [ ] `make agent-check` passes with `mistralai-workflows` NOT installed + (no spurious imports) +- [ ] Layer-1 tests pass on a no-extras venv +- [ ] Layer-2 tests pass on `[dev,mistralai-workflows]` (DIRECT mode e2e) +- [ ] Layer-3 tests pass on `[dev,mistralai-workflows,temporal]` + (TEMPORAL_BLOCKING + FIRE_AND_FORGET) +- [ ] `pipelex_run_pipe` round-trips a pipe with dynamic-concept output + via `library_crate_dump` +- [ ] Documentation published; CHANGELOG entry merged +- [ ] Cookbook example added + +--- + +## 11. Risks / open items (track but don't block) + +- [ ] FIRE_AND_FORGET footgun mitigation — validation in + `run_pipe_via_bridge` before mode dispatch (covered in design) +- [ ] Pipelex bootstrap inside an already-bootstrapped Mistral worker — + confirm singleton guard is reentrant (pre-flight) +- [ ] Concurrent activities sharing process-global Pipelex state — reuse + per-call library scoping from + `pipelex/temporal/tprl_pipe/wf_pipe_router.py`; verify no leakage + under concurrent activity load +- [ ] Mistral's payload converter calls `params.model_dump()` at the + *workflow* call site only; activities use the converter directly. + Our boundary is JSON-only, so no kajson preservation needed — + confirm by integration test with a pipe that produces a + dynamic-concept output +- [ ] If Mistral upgrades break our use of `OffloadableField` location, + revisit (currently `mistralai.extra.workflows`) +- [ ] **Goal 1 deferred**: porting Pipelex orchestration to Mistral + Workflows as an alternative durable runtime is blocked on Mistral + adding extension hooks for payload converter, codec, sandbox + passthrough, and a bare-Temporal `run_worker` mode. File issues + with the Mistral team if/when we want to revisit. + +--- + +## 12. Resuming a session + +1. Read this file end-to-end. +2. Find the first unchecked box. If it's in §7 (pre-flight), resolve those + first — they may change the design (e.g. guard placement decision). +3. Implement the next phase's items in order, checking off boxes as you go. +4. Update §3 (locked-in decisions) only when an explicit user decision + changes the design; otherwise the design in §4–§6 is authoritative. +5. After each phase, run `make agent-check` and `make agent-test` (with + the appropriate extras installed for the phase) before moving on. diff --git a/pipelex/plugins/mistral/mistral_config.py b/pipelex/plugins/mistral/mistral_config.py index 1c257cb77..9a937eb2f 100644 --- a/pipelex/plugins/mistral/mistral_config.py +++ b/pipelex/plugins/mistral/mistral_config.py @@ -9,7 +9,7 @@ from pipelex.types import StrEnum if TYPE_CHECKING: - from mistralai.models import MistralPromptMode + from mistralai.client.models import MistralPromptMode from pipelex.cogt.llm.llm_job_components import ReasoningEffort diff --git a/pipelex/plugins/mistral/mistral_extract_worker.py b/pipelex/plugins/mistral/mistral_extract_worker.py index 1034ed830..833d6ad9f 100644 --- a/pipelex/plugins/mistral/mistral_extract_worker.py +++ b/pipelex/plugins/mistral/mistral_extract_worker.py @@ -1,6 +1,7 @@ from typing import Any -from mistralai import Mistral, MistralError +from mistralai.client import Mistral +from mistralai.client.errors import MistralError from typing_extensions import override from pipelex.cogt.exceptions import ExtractCapabilityError, ExtractJobFailureError, InferenceErrorCategory, SdkTypeError diff --git a/pipelex/plugins/mistral/mistral_factory.py b/pipelex/plugins/mistral/mistral_factory.py index 1644975fb..5cc76f36b 100644 --- a/pipelex/plugins/mistral/mistral_factory.py +++ b/pipelex/plugins/mistral/mistral_factory.py @@ -4,15 +4,16 @@ from typing import TYPE_CHECKING import aiofiles -import mistralai -from mistralai import Mistral -from mistralai.models import ( +from mistralai.client import Mistral +from mistralai.client.models import ( + ChatCompletionRequestMessage, ContentChunk, DocumentURLChunk, DocumentURLChunkTypedDict, ImageURLChunk, ImageURLChunkTypedDict, - Messages, + OCRImageObject, + OCRResponse, SystemMessage, TextChunk, UsageInfo, @@ -55,9 +56,9 @@ def make_mistral_client( # Message ######################################################### - async def make_simple_messages(self, llm_job: LLMJob) -> list[Messages]: + async def make_simple_messages(self, llm_job: LLMJob) -> list[ChatCompletionRequestMessage]: """Makes a list of messages with a system message (if provided) and followed by a user message.""" - messages: list[Messages] = [] + messages: list[ChatCompletionRequestMessage] = [] user_content: list[ContentChunk] = [] if user_text := llm_job.llm_prompt.user_text: user_content.append(TextChunk(text=user_text)) @@ -181,7 +182,7 @@ def make_nb_tokens_by_category(self, usage: UsageInfo) -> NbTokensByCategoryDict @classmethod async def make_extract_output_from_mistral_response( cls, - mistral_extract_response: mistralai.OCRResponse, + mistral_extract_response: OCRResponse, ) -> ExtractOutput: """Convert Mistral OCR response to ExtractOutput. @@ -262,7 +263,7 @@ def _clean_mistral_image_base64(cls, base64_str: str) -> str: @classmethod def make_extracted_image_from_page_from_mistral_ocr_image_obj( cls, - mistral_ocr_image_obj: mistralai.OCRImageObject, + mistral_ocr_image_obj: OCRImageObject, ) -> ExtractedImageFromPage: if not mistral_ocr_image_obj.image_base64: msg = "Mistral OCR image object does not have an image base64" diff --git a/pipelex/plugins/mistral/mistral_llm_worker.py b/pipelex/plugins/mistral/mistral_llm_worker.py index 7bc229a18..9d6a945a6 100644 --- a/pipelex/plugins/mistral/mistral_llm_worker.py +++ b/pipelex/plugins/mistral/mistral_llm_worker.py @@ -1,8 +1,9 @@ from typing import TYPE_CHECKING, Any -from mistralai import Mistral, MistralError -from mistralai.models import MistralPromptMode, TextChunk, ThinkChunk -from mistralai.types import UNSET +from mistralai.client import Mistral +from mistralai.client.errors import MistralError +from mistralai.client.models import MistralPromptMode, TextChunk, ThinkChunk +from mistralai.client.types import UNSET from typing_extensions import override from pipelex import log @@ -24,8 +25,8 @@ from pipelex.urls import URLs if TYPE_CHECKING: - from mistralai.models import ChatCompletionResponse - from mistralai.types import OptionalNullable + from mistralai.client.models import ChatCompletionResponse + from mistralai.client.types import OptionalNullable class MistralLLMWorker(LLMWorkerInternalAbstract): @@ -176,7 +177,11 @@ async def _gen_text( if not response.choices: msg = "Mistral response.choices is None" raise LLMCompletionError(msg) - mistral_response_content = response.choices[0].message.content + message = response.choices[0].message + if message is None: + msg = "Mistral response.choices[0].message is None" + raise LLMCompletionError(msg) + mistral_response_content = message.content result_text: str if isinstance(mistral_response_content, str): result_text = mistral_response_content diff --git a/pipelex/plugins/mistral/mistral_llms.py b/pipelex/plugins/mistral/mistral_llms.py index 2d57da586..7edb3c427 100644 --- a/pipelex/plugins/mistral/mistral_llms.py +++ b/pipelex/plugins/mistral/mistral_llms.py @@ -1,11 +1,11 @@ -from mistralai.models import Data +from mistralai.client.models import BaseModelCard, FTModelCard from pipelex.hub import get_models_manager from pipelex.plugins.mistral.mistral_exceptions import MistralModelListingError from pipelex.plugins.mistral.mistral_factory import MistralFactory -def mistral_list_available_models() -> list[Data]: +def mistral_list_available_models() -> list[BaseModelCard | FTModelCard]: backend = get_models_manager().get_required_inference_backend("mistral") mistral_client = MistralFactory.make_mistral_client(backend=backend) models_list_response = mistral_client.models.list() @@ -16,4 +16,5 @@ def mistral_list_available_models() -> list[Data]: if not models_list: msg = "No models found" raise MistralModelListingError(msg) - return sorted(models_list, key=lambda model: model.id) + known_models = [model for model in models_list if isinstance(model, (BaseModelCard, FTModelCard))] + return sorted(known_models, key=lambda model: model.id) diff --git a/pipelex/plugins/mistralai_workflows/__init__.py b/pipelex/plugins/mistralai_workflows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pipelex/plugins/mistralai_workflows/activities.py b/pipelex/plugins/mistralai_workflows/activities.py new file mode 100644 index 000000000..85537e470 --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/activities.py @@ -0,0 +1,40 @@ +"""Tier 1 — pre-decorated Mistral Workflows activity that runs a Pipelex pipe. + +Importing this module triggers the optional-dep guard: if +``mistralai-workflows`` is not installed, the import fails fast with a +``MistralWorkflowsNotInstalledError`` carrying install instructions. +""" + +from datetime import timedelta + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError + +try: + from mistralai.workflows import activity +except ImportError as exc: + msg = ( + "The 'mistralai-workflows' optional dependency is required to use " + "pipelex.plugins.mistralai_workflows.activities. " + "Install with: pip install 'pipelex[mistralai-workflows]'" + ) + raise MistralWorkflowsNotInstalledError(msg) from exc + + +@activity( + start_to_close_timeout=timedelta(minutes=10), + retry_policy_max_attempts=3, +) +async def pipelex_run_pipe(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: + """Run a Pipelex pipe from inside a Mistral Workflows activity. + + Thin wrapper around ``run_pipe_via_bridge`` so users get a ready-to-register + activity without having to write their own ``@activity`` decoration. For + custom timeouts, retry policies, rate limits, or sticky-to-worker config, + call ``run_pipe_via_bridge`` directly from your own ``@activity`` (Tier 2). + """ + return await run_pipe_via_bridge(input_payload) diff --git a/pipelex/plugins/mistralai_workflows/bootstrap.py b/pipelex/plugins/mistralai_workflows/bootstrap.py new file mode 100644 index 000000000..b607aeac3 --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/bootstrap.py @@ -0,0 +1,39 @@ +"""Idempotent Pipelex boot helpers for use inside Mistral Workflows activities. + +Pipelex's own ``Pipelex.make()`` raises if a singleton already exists. The +activity boundary is a hot path that can be reached from many concurrent +activities, so we wrap the boot in an idempotent guard so callers don't have +to think about it. +""" + +from typing import Any, Callable + +from pipelex.pipelex import Pipelex + + +def ensure_pipelex_booted( + config_overrides: dict[str, Any] | None = None, +) -> None: + """Boot Pipelex on first call; no-op if already initialized. + + Idempotent. Safe to call from inside an activity; safe to call from a + worker entry-point before activities start. If a Pipelex singleton was + already created externally (e.g. via the user's worker bootstrap), this + function adopts that singleton without re-initializing. + """ + if Pipelex.get_optional_instance() is None: + Pipelex.make(config_overrides=config_overrides) + + +def get_pipelex_dependency() -> Callable[[], Pipelex]: + """Return a callable suitable for ``mistralai.workflows.Depends(...)``. + + Booting on first resolve so the dependency is cheap to declare per-activity + without forcing eager init at worker start. + """ + + def _resolver() -> Pipelex: + ensure_pipelex_booted() + return Pipelex.get_instance() + + return _resolver diff --git a/pipelex/plugins/mistralai_workflows/bridge.py b/pipelex/plugins/mistralai_workflows/bridge.py new file mode 100644 index 000000000..61848fb2e --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/bridge.py @@ -0,0 +1,313 @@ +"""Framework-agnostic core of the mistralai_workflows plugin. + +This module contains the boundary types (``PipelexPipeRunInput`` / +``PipelexPipeRunOutput``) and the dispatch entry-point +(``run_pipe_via_bridge``) used by the Mistral Workflows activity wrapper. It +deliberately does NOT import ``mistralai.workflows`` at module top-level so +that callers can use the bridge directly (Tier 3 usage) and so that unit tests +can exercise it on a venv without the optional dep installed. + +The Temporal extra is lazy-imported only inside the temporal-mode branches. +""" + +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, AsyncGenerator +from uuid import uuid4 + +import shortuuid +from pydantic import BaseModel, ConfigDict, Field + +from pipelex.core.memory.working_memory import MAIN_STUFF_NAME +from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory +from pipelex.hub import ( + get_library_manager, + get_required_pipe, + set_current_library, + teardown_current_library, +) +from pipelex.libraries.library_crate import LibraryCrate +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.pipe_run.exceptions import PipeJobError, PipeRouterError, PipeRunError +from pipelex.pipe_run.pipe_job_factory import PipeJobFactory +from pipelex.pipe_run.pipe_router import PipeRouter +from pipelex.pipe_run.pipe_run import PipeRun +from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory +from pipelex.pipeline.exceptions import PipeExecutionError, PipelineExecutionError +from pipelex.pipeline.job_metadata import JobMetadata +from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted +from pipelex.plugins.mistralai_workflows.exceptions import ( + MissingPipelexTemporalExtraError, + PipelexBridgeRuntimeError, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.system.telemetry.otel_constants import OTelConstants + +if TYPE_CHECKING: + from pipelex.core.memory.working_memory import WorkingMemory + from pipelex.core.pipes.pipe_output import PipeOutput + from pipelex.pipe_run.pipe_job import PipeJob + + +class PipelexPipeRunInput(BaseModel): + """JSON-safe input crossing the Mistral/Temporal boundary.""" + + model_config = ConfigDict(extra="forbid") + + pipe_code: str + inputs: dict[str, Any] = Field(default_factory=dict) + output_name: str | None = None + pipeline_run_id: str | None = None + user_id: str | None = None + library_crate_dump: dict[str, Any] | None = None + execution_mode: PipelexExecutionMode = PipelexExecutionMode.DIRECT + delivery_assignment_dump: dict[str, Any] | None = None + + +class PipelexPipeRunOutput(BaseModel): + """JSON-safe output crossing the Mistral/Temporal boundary.""" + + model_config = ConfigDict(extra="forbid") + + output_dict: dict[str, Any] + main_stuff_name: str | None = None + pipeline_run_id: str + workflow_id: str | None = None + is_completed: bool + graph_spec_dump: dict[str, Any] | None = None + + +async def run_pipe_via_bridge(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: + """Run a Pipelex pipe from inside a Mistral Workflows activity. + + Booting Pipelex on first call (no-op if already initialized); validating + the input; opening a per-call scoped library if a ``library_crate_dump`` + is provided; then dispatching to the requested execution mode. + """ + ensure_pipelex_booted() + _validate_input(input_payload) + + library_crate = _decode_library_crate(input_payload.library_crate_dump) + delivery_assignment = _decode_delivery_assignment(input_payload.delivery_assignment_dump) + + async with _scoped_library_for_crate(library_crate): + pipe_job = build_pipe_job_from_input(input_payload=input_payload, library_crate=library_crate) + + match input_payload.execution_mode: + case PipelexExecutionMode.DIRECT: + return await _run_direct(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + case PipelexExecutionMode.TEMPORAL_BLOCKING: + _require_pipelex_temporal_extra() + return await _run_temporal_blocking(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + _require_pipelex_temporal_extra() + return await _run_temporal_fire_and_forget(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + + +def build_pipe_job_from_input( + input_payload: PipelexPipeRunInput, + library_crate: LibraryCrate | None, +) -> PipeJob: + """Hydrate a PipeJob from JSON-safe input. + + Looks up the pipe in the active library; the caller is responsible for + making sure the active library contains the pipe (by passing a + ``library_crate_dump`` or pre-loading the library at boot). + """ + pipe = get_required_pipe(pipe_code=input_payload.pipe_code) + + pipeline_run_id = input_payload.pipeline_run_id or shortuuid.uuid() + + working_memory: WorkingMemory + if input_payload.inputs: + working_memory = WorkingMemoryFactory.make_from_pipeline_inputs( + pipeline_inputs=input_payload.inputs, + search_domain_codes=[pipe.domain_code], + ) + else: + working_memory = WorkingMemoryFactory.make_empty() + + job_metadata = JobMetadata( + user_id=input_payload.user_id or OTelConstants.DEFAULT_USER_ID, + pipeline_run_id=pipeline_run_id, + ) + pipe_run_params = PipeRunParamsFactory.make_run_params() + + return PipeJobFactory.make_pipe_job( + pipe=pipe, + pipe_run_params=pipe_run_params, + job_metadata=job_metadata, + working_memory=working_memory, + output_name=input_payload.output_name, + library_crate=library_crate, + ) + + +def serialize_pipe_output(pipe_output: PipeOutput) -> dict[str, Any]: + """Dehydrate a PipeOutput's working memory to a JSON-safe dict. + + Always uses ``WorkingMemory.dump_for_temporal()`` — the same format Pipelex + uses internally for Temporal transit. The shape is stable regardless of + whether a ``library_crate`` was attached: + ``{"root": {stuff_name: {"content": {...}, ...}}, "aliases": {...}}``. + + Type metadata embedded by ``dump_for_temporal`` lets callers reconstruct a + typed ``WorkingMemory`` when they have the matching class registry in + scope (e.g. via ``hydrate_working_memory``). + """ + return pipe_output.working_memory.dump_for_temporal() + + +def _validate_input(input_payload: PipelexPipeRunInput) -> None: + if input_payload.execution_mode is PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET and input_payload.delivery_assignment_dump is None: + msg = ( + "PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET requires a delivery_assignment_dump; " + "otherwise the pipe completion would be silently dropped." + ) + raise PipelexBridgeRuntimeError(msg) + + +def _decode_library_crate(library_crate_dump: dict[str, Any] | None) -> LibraryCrate | None: + if library_crate_dump is None: + return None + return LibraryCrate.model_validate(library_crate_dump) + + +def _decode_delivery_assignment(delivery_assignment_dump: dict[str, Any] | None) -> DeliveryAssignment | None: + if delivery_assignment_dump is None: + return None + return DeliveryAssignment.model_validate(delivery_assignment_dump) + + +@asynccontextmanager +async def _scoped_library_for_crate(library_crate: LibraryCrate | None) -> AsyncGenerator[str | None, None]: # noqa: RUF029 + """Open a per-call scoped library for the duration of a pipe run. + + When ``library_crate`` is None, this is a no-op: callers fall back to the + library that was loaded into the active class registry at boot. When + provided, opens a fresh library, loads the crate into it, sets it as the + current library for the duration of the pipe execution, and tears it down + on the way out. + """ + if library_crate is None: + yield None + return + + library_manager = get_library_manager() + library_id = f"mistralai_workflows_{uuid4().hex[:8]}" + library_manager.open_library(library_id=library_id) + set_current_library(library_id=library_id) + try: + library_manager.load_from_crate(library_id=library_id, crate=library_crate) + yield library_id + finally: + library_manager.teardown(library_id=library_id) + teardown_current_library() + + +async def _run_direct( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + pipe_run = PipeRun(pipe_router=PipeRouter()) + try: + pipe_output = await pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe execution failed in DIRECT mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return _serialize_completed_output( + pipe_output=pipe_output, + pipe_job=pipe_job, + workflow_id=None, + ) + + +async def _run_temporal_blocking( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 + + temporal_pipe_run = make_temporal_pipe_run() + try: + pipe_output = await temporal_pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe execution failed in TEMPORAL_BLOCKING mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return _serialize_completed_output( + pipe_output=pipe_output, + pipe_job=pipe_job, + workflow_id=pipe_output.pipeline_run_id, + ) + + +async def _run_temporal_fire_and_forget( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 + + temporal_pipe_run = make_temporal_pipe_run() + try: + workflow_id, _handle = await temporal_pipe_run.start(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe dispatch failed in TEMPORAL_FIRE_AND_FORGET mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return PipelexPipeRunOutput( + output_dict={}, + main_stuff_name=None, + pipeline_run_id=pipe_job.job_metadata.pipeline_run_id, + workflow_id=workflow_id, + is_completed=False, + graph_spec_dump=None, + ) + + +def _serialize_completed_output( + pipe_output: PipeOutput, + pipe_job: PipeJob, # noqa: ARG001 — kept for symmetry with future per-crate serialization tweaks + workflow_id: str | None, +) -> PipelexPipeRunOutput: + output_dict = serialize_pipe_output(pipe_output=pipe_output) + + main_stuff_name = _resolve_main_stuff_root_key(pipe_output=pipe_output) + + graph_spec_dump = pipe_output.graph_spec.model_dump(mode="json") if pipe_output.graph_spec is not None else None + + return PipelexPipeRunOutput( + output_dict=output_dict, + main_stuff_name=main_stuff_name, + pipeline_run_id=pipe_output.pipeline_run_id, + workflow_id=workflow_id, + is_completed=True, + graph_spec_dump=graph_spec_dump, + ) + + +def _resolve_main_stuff_root_key(pipe_output: PipeOutput) -> str | None: + """Return the actual ``root`` dict key under which the main stuff lives. + + The main stuff can either sit directly at ``root[MAIN_STUFF_NAME]`` or be + referenced via ``aliases[MAIN_STUFF_NAME]`` pointing at its real name. + Callers indexing the output_dict need the actual root key, not the + stuff's display ``stuff_name``. + """ + working_memory = pipe_output.working_memory + if MAIN_STUFF_NAME in working_memory.root: + return MAIN_STUFF_NAME + aliased_target = working_memory.aliases.get(MAIN_STUFF_NAME) + if aliased_target is not None and aliased_target in working_memory.root: + return aliased_target + return None + + +def _require_pipelex_temporal_extra() -> None: + try: + import temporalio # noqa: F401, PLC0415 + except ImportError as exc: + msg = "TEMPORAL_* execution modes require the pipelex[temporal] extra. Install with: pip install 'pipelex[temporal,mistralai-workflows]'" + raise MissingPipelexTemporalExtraError(msg) from exc diff --git a/pipelex/plugins/mistralai_workflows/exceptions.py b/pipelex/plugins/mistralai_workflows/exceptions.py new file mode 100644 index 000000000..7d04d7908 --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/exceptions.py @@ -0,0 +1,17 @@ +from pipelex.base_exceptions import PipelexError + + +class MistralWorkflowsPluginError(PipelexError): + """Base for errors raised by the mistralai-workflows plugin.""" + + +class MistralWorkflowsNotInstalledError(MistralWorkflowsPluginError, ImportError): + """Raised when the optional `mistralai-workflows` dependency is missing.""" + + +class MissingPipelexTemporalExtraError(MistralWorkflowsPluginError): + """Raised when a TEMPORAL_* execution mode is requested without the pipelex[temporal] extra.""" + + +class PipelexBridgeRuntimeError(MistralWorkflowsPluginError): + """Raised when a pipe execution dispatched through the bridge fails.""" diff --git a/pipelex/plugins/mistralai_workflows/execution_mode.py b/pipelex/plugins/mistralai_workflows/execution_mode.py new file mode 100644 index 000000000..24f3f6196 --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/execution_mode.py @@ -0,0 +1,37 @@ +from pipelex.types import StrEnum + + +class PipelexExecutionMode(StrEnum): + """How a Pipelex pipe runs inside a Mistral Workflows activity. + + DIRECT: in-process; no Temporal involved on Pipelex's side; activity blocks + until the pipe completes. Fastest feedback, simplest ops. + TEMPORAL_BLOCKING: dispatch the pipe as a Pipelex Temporal workflow; the + activity awaits completion. Pipe runs durably on Pipelex's worker + fleet. Requires the pipelex[temporal] extra. + TEMPORAL_FIRE_AND_FORGET: dispatch the pipe as a Pipelex Temporal workflow + and return immediately with the workflow_id. Activity does NOT wait; + completion is signalled out-of-band via DeliveryAssignment (webhook / + storage). Same dep requirements as TEMPORAL_BLOCKING. + ``delivery_assignment_dump`` is required. + """ + + DIRECT = "direct" + TEMPORAL_BLOCKING = "temporal_blocking" + TEMPORAL_FIRE_AND_FORGET = "temporal_fire_and_forget" + + @property + def requires_pipelex_temporal(self) -> bool: + match self: + case PipelexExecutionMode.DIRECT: + return False + case PipelexExecutionMode.TEMPORAL_BLOCKING | PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + return True + + @property + def is_fire_and_forget(self) -> bool: + match self: + case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + return True + case PipelexExecutionMode.DIRECT | PipelexExecutionMode.TEMPORAL_BLOCKING: + return False diff --git a/pyproject.toml b/pyproject.toml index cb51e9306..42118e4ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,7 @@ google-genai = [ ] huggingface = ["huggingface_hub>=0.23,<1.0.0"] linkup = ["linkup-sdk>=0.12.0"] -mistralai = ["mistralai>=1.12.0"] +mistralai = ["mistralai>=2.4.4"] mistralai-workflows = ["mistralai-workflows>=3.3.0"] dynamodb = ["boto3>=1.34.131"] s3 = ["boto3>=1.34.131", "aioboto3>=13.4.0"] @@ -151,6 +151,18 @@ module = [ "pypdfium2.raw", ] +[[tool.mypy.overrides]] +# Mistral Workflows uses PEP 695 type-parameter syntax that mypy rejects under +# python_version=3.11 even though our runtime supports 3.10+. Skip following +# imports into this third-party package so its source files don't leak into +# our type-check. +follow_imports = "skip" +ignore_errors = true +module = [ + "mistralai.workflows.*", + "mistralai.workflows", +] + [tool.pyright] pythonVersion = "3.11" include = ["pipelex", "tests"] @@ -293,6 +305,7 @@ exclude_lines = [ [tool.ruff] exclude = [ + ".claude/skills/workflows", ".cursor", ".git", ".github", @@ -440,6 +453,11 @@ convention = "google" [tool.uv] required-version = ">=0.7.2" +[tool.uv.sources] +# Temporary: pin to fork that adds mistralai 2.x support. Revert to PyPI release once +# https://github.com/567-labs/instructor/pull/2298 is merged and published. +instructor = { git = "https://github.com/Ian321/instructor.git", rev = "0efd9c09b05ef561defff3a8b86fe86e5e61214c" } + [tool.hatch.build.targets.wheel] packages = ["pipelex"] exclude = ["pipelex/cli/dev_cli"] diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py b/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py new file mode 100644 index 000000000..085a8a240 --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py @@ -0,0 +1,35 @@ +from collections.abc import Generator +from pathlib import Path + +import pytest + +from pipelex.hub import get_func_registry, get_library_manager, set_current_library +from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs import mistralai_workflows_bridge_echo + +TEST_DATA_DIR = Path(__file__).parent / "test_data" + + +@pytest.fixture(scope="class") +def bridge_test_library() -> Generator[str, None, None]: + """Open a class-scoped library populated with the bridge test pipe. + + The pipe ``mistralai_workflows_bridge_test.bridge_func_pipe`` is registered + in the active library, and the matching Python function is registered in + the FuncRegistry. Both are torn down on exit. + """ + func_registry = get_func_registry() + func_registry.register_function(mistralai_workflows_bridge_echo) + + library_manager = get_library_manager() + library_id, _ = library_manager.open_library() + set_current_library(library_id=library_id) + library_manager.load_libraries( + library_id=library_id, + library_dirs=[TEST_DATA_DIR], + ) + try: + yield library_id + finally: + library_manager.teardown(library_id=library_id) + if func_registry.has_function("mistralai_workflows_bridge_echo"): + func_registry.unregister_function_by_name("mistralai_workflows_bridge_echo") diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py new file mode 100644 index 000000000..4f4d3a8bd --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py @@ -0,0 +1,112 @@ +"""Layer-2 integration test: ``pipelex_run_pipe`` activity end-to-end. + +Spins an in-process Temporal test environment plus a Mistral test worker, +and runs a workflow that invokes ``pipelex_run_pipe`` against a real loaded +Pipelex pipe. Skipped when ``mistralai-workflows`` is not installed. +""" + +from typing import Any + +import pytest +import pytest_asyncio + +mistralai_workflows = pytest.importorskip("mistralai.workflows") + +from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 +from mistralai.workflows.testing import create_test_worker # noqa: E402 # pyright: ignore[reportUnknownVariableType] +from temporalio.common import SearchAttributeKey # noqa: E402 +from temporalio.testing import WorkflowEnvironment # noqa: E402 + +# Pipelex imports must be wrapped in ``imports_passed_through`` because the +# workflow sandbox would otherwise reject our pipelex imports while validating +# the workflow class. Activities themselves run outside the sandbox so the +# wrapped imports are only needed where the workflow body references them. +with mistralai_workflows.workflow.unsafe.imports_passed_through(): + from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe + from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + ) + from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" +TEST_TASK_QUEUE = "pipelex-mistralai-workflows-test" + + +@mistralai_workflows.workflow.define( + name="pipelex-bridge-test-workflow", + enforce_determinism=False, # bypass workflow sandbox for the integration test +) +class PipelexBridgeTestWorkflow: + @mistralai_workflows.workflow.entrypoint + async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: + payload = PipelexPipeRunInput.model_validate(payload_dict) + output: PipelexPipeRunOutput = await pipelex_run_pipe(payload) + return output + + +@pytest.fixture(scope="module", autouse=True) +def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] + """Pin Mistral's global task_queue config to our test queue. + + Mistral's ``@activity`` wrapper dispatches via + ``temporalio.workflow.execute_activity(..., task_queue=config.get_effective_task_queue())``, + which reads the global ``mistralai_config.temporal.task_queue`` (default + ``"default"``). If we don't override it, activities are scheduled on + ``"default"`` while the worker polls ``TEST_TASK_QUEUE`` — the activity + never gets picked up and the workflow hangs. + """ + original = mistralai_config.temporal.task_queue + mistralai_config.temporal.task_queue = TEST_TASK_QUEUE + try: + yield + finally: + mistralai_config.temporal.task_queue = original + + +@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] +async def workflow_env(): + # Mistral's workflow.define wraps the run method with code that upserts an + # ``OtelTraceId`` search attribute on every workflow run. The dev server + # rejects the workflow activation if the attribute isn't pre-registered on + # the namespace, so we declare it here. + env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] + search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], + ) + try: + yield env + finally: + await env.shutdown() + + +@pytest.mark.asyncio(loop_scope="class") +class TestPipelexRunPipeActivity: + async def test_workflow_invokes_pipe_via_bridge_in_direct_mode( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + payload = PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "via mistralai workflow"}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + + async with create_test_worker( + workflow_env, + workflows=[PipelexBridgeTestWorkflow], + activities=[pipelex_run_pipe], + task_queue=TEST_TASK_QUEUE, + ): + result_dict = await workflow_env.client.execute_workflow( + PipelexBridgeTestWorkflow.run, + {"payload_dict": payload.model_dump(mode="json")}, + id="pipelex-bridge-test-workflow-direct", + task_queue=TEST_TASK_QUEUE, + ) + + result = PipelexPipeRunOutput.model_validate(result_dict) + assert result.is_completed is True + assert result.workflow_id is None + assert result.main_stuff_name is not None + assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "via mistralai workflow" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py new file mode 100644 index 000000000..77155d49e --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py @@ -0,0 +1,85 @@ +"""Layer-1 integration tests for the mistralai_workflows bridge in DIRECT mode. + +These tests do NOT depend on the optional ``mistralai-workflows`` package — they +exercise only the framework-agnostic core (``run_pipe_via_bridge`` with a real +loaded pipe). The activity wrapper is covered separately in +``test_activities_direct.py``, which DOES require the optional dep. +""" + +from typing import Any + +import pytest + +from pipelex.hub import get_library_manager +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, run_pipe_via_bridge +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" + + +@pytest.mark.asyncio(loop_scope="class") +class TestBridgeDirect: + async def test_direct_mode_with_globally_loaded_library( + self, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Bridge runs a pipe found in the active library when no crate is provided.""" + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "hello world"}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.workflow_id is None + assert result.main_stuff_name is not None + main_stuff_dump = result.output_dict["root"][result.main_stuff_name] + assert main_stuff_dump["content"]["text"] == "hello world" + + async def test_direct_mode_with_library_crate_dump( + self, + bridge_test_library: str, + ) -> None: + """Bridge round-trips through ``library_crate_dump`` end-to-end. + + Captures a LibraryCrate from the loaded library, pipes it through the + bridge as a JSON-safe dict, and verifies the pipe still resolves and + runs against the per-call scoped library that the bridge opens. + """ + crate = get_library_manager().get_crate(library_id=bridge_test_library) + assert crate is not None + crate_dump: dict[str, Any] = crate.model_dump(mode="json") + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "via crate"}, + library_crate_dump=crate_dump, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.main_stuff_name is not None + main_stuff_dump = result.output_dict["root"][result.main_stuff_name] + assert main_stuff_dump["content"]["text"] == "via crate" + + async def test_direct_mode_uses_caller_pipeline_run_id( + self, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Caller-supplied ``pipeline_run_id`` propagates to the PipeJob.""" + caller_run_id = "caller-supplied-run-id" + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "trace me"}, + pipeline_run_id=caller_run_id, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.pipeline_run_id == caller_run_id diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py new file mode 100644 index 000000000..78caeb56b --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py @@ -0,0 +1,14 @@ +"""Test functions registered for the mistralai_workflows bridge integration tests.""" + +from pipelex.core.memory.working_memory import WorkingMemory +from pipelex.core.stuffs.text_content import TextContent + + +def mistralai_workflows_bridge_echo(working_memory: WorkingMemory) -> TextContent: + """Echo the ``input_text`` stuff back as a TextContent output. + + Used by tests/integration/pipelex/plugins/mistralai_workflows to validate + end-to-end pipe execution through the bridge without invoking inference. + """ + input_text = working_memory.get_stuff_as_str("input_text") + return TextContent(text=input_text) diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds new file mode 100644 index 000000000..bf6b02f21 --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds @@ -0,0 +1,8 @@ +domain = "mistralai_workflows_bridge_test" +description = "Test pipes for the mistralai_workflows plugin bridge" + +[pipe.bridge_func_pipe] +type = "PipeFunc" +description = "Echoes the input text back as output" +output = "Text" +function_name = "mistralai_workflows_bridge_echo" diff --git a/tests/unit/pipelex/plugins/mistral/test_mistral_reasoning.py b/tests/unit/pipelex/plugins/mistral/test_mistral_reasoning.py index ad973c9d3..13cdc466d 100644 --- a/tests/unit/pipelex/plugins/mistral/test_mistral_reasoning.py +++ b/tests/unit/pipelex/plugins/mistral/test_mistral_reasoning.py @@ -101,7 +101,7 @@ def test_thinking_mode_adaptive_raises_capability_error(self, mocker: MockerFixt def test_no_reasoning_params_returns_unset(self, mocker: MockerFixture): """When neither reasoning_effort nor reasoning_budget is set, returns UNSET.""" - from mistralai.types import UNSET # noqa: PLC0415 + from mistralai.client.types import UNSET # noqa: PLC0415 worker = _make_worker(mocker, thinking_mode=ThinkingMode.MANUAL) job_params = LLMJobParams(temperature=0.5) diff --git a/tests/unit/pipelex/plugins/mistral/test_mistral_worker_error_handling.py b/tests/unit/pipelex/plugins/mistral/test_mistral_worker_error_handling.py index 230e501d7..341d48c6a 100644 --- a/tests/unit/pipelex/plugins/mistral/test_mistral_worker_error_handling.py +++ b/tests/unit/pipelex/plugins/mistral/test_mistral_worker_error_handling.py @@ -6,7 +6,7 @@ import httpx import pytest -from mistralai import MistralError +from mistralai.client.errors import MistralError if TYPE_CHECKING: from pytest_mock import MockerFixture diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py new file mode 100644 index 000000000..efd163fcd --- /dev/null +++ b/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py @@ -0,0 +1,121 @@ +import pytest +from pytest_mock import MockerFixture + +from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory +from pipelex.core.pipes.pipe_output import PipeOutput +from pipelex.pipe_run.pipe_job import PipeJob +from pipelex.pipe_run.pipe_run import PipeRun +from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory +from pipelex.pipeline.job_metadata import JobMetadata +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, run_pipe_via_bridge +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + + +def _make_fake_pipe_job(mocker: MockerFixture, pipe_code: str, pipeline_run_id: str) -> PipeJob: + """Build a PipeJob without triggering Pydantic's PipeAbstract validation. + + Tests at the dispatch layer don't care about the concrete pipe — only + that the bridge routes the right pipe_job to the right PipeRun. Using + ``model_construct`` lets us pass a MagicMock as ``pipe`` without + constructing a full PipeAbstract subclass. + """ + fake_pipe = mocker.MagicMock() + fake_pipe.code = pipe_code + fake_pipe.domain_code = "fake_domain" + return PipeJob.model_construct( + pipe=fake_pipe, + working_memory=WorkingMemoryFactory.make_empty(), + pipe_run_params=PipeRunParamsFactory.make_run_params(), + job_metadata=JobMetadata(user_id="anonymous", pipeline_run_id=pipeline_run_id), + library_crate=None, + ) + + +@pytest.mark.asyncio +class TestDispatch: + async def test_direct_mode_calls_pipe_run_with_pipe_job(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_output = PipeOutput( + working_memory=WorkingMemoryFactory.make_empty(), + pipeline_run_id="injected-run-id", + ) + mock_run = mocker.patch.object(PipeRun, "run", new_callable=mocker.AsyncMock, return_value=fake_output) + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.DIRECT, + pipeline_run_id="caller-run-id", + ) + ) + + assert mock_run.await_count == 1 + await_args = mock_run.await_args + assert await_args is not None + call_kwargs: dict[str, object] = dict(await_args.kwargs) + assert call_kwargs["delivery_assignment"] is None + assert call_kwargs["pipe_job"] is fake_job + + assert result.is_completed is True + assert result.pipeline_run_id == "injected-run-id" + assert result.workflow_id is None + assert result.graph_spec_dump is None + + async def test_temporal_blocking_dispatches_to_temporal_pipe_run(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_output = PipeOutput( + working_memory=WorkingMemoryFactory.make_empty(), + pipeline_run_id="temporal-run-id", + ) + fake_temporal_run = mocker.AsyncMock(return_value=fake_output) + fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") + fake_factory.return_value.run = fake_temporal_run + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, + ) + ) + + fake_factory.assert_called_once() + assert fake_temporal_run.await_count == 1 + assert result.is_completed is True + assert result.workflow_id == "temporal-run-id" + + async def test_temporal_fire_and_forget_returns_workflow_id_without_completion(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_handle = mocker.MagicMock() + fake_start = mocker.AsyncMock(return_value=("wf-id-42", fake_handle)) + fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") + fake_factory.return_value.start = fake_start + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump={"webhooks": [], "storage": None}, + pipeline_run_id="caller-run-id", + ) + ) + + fake_start.assert_awaited_once() + assert result.is_completed is False + assert result.workflow_id == "wf-id-42" + assert result.pipeline_run_id == "caller-run-id" + assert result.output_dict == {} diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py new file mode 100644 index 000000000..930e3cf89 --- /dev/null +++ b/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py @@ -0,0 +1,18 @@ +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + + +class TestPipelexExecutionMode: + def test_string_values_are_stable(self): + assert PipelexExecutionMode.DIRECT == "direct" + assert PipelexExecutionMode.TEMPORAL_BLOCKING == "temporal_blocking" + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET == "temporal_fire_and_forget" + + def test_requires_pipelex_temporal(self): + assert PipelexExecutionMode.DIRECT.requires_pipelex_temporal is False + assert PipelexExecutionMode.TEMPORAL_BLOCKING.requires_pipelex_temporal is True + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.requires_pipelex_temporal is True + + def test_is_fire_and_forget(self): + assert PipelexExecutionMode.DIRECT.is_fire_and_forget is False + assert PipelexExecutionMode.TEMPORAL_BLOCKING.is_fire_and_forget is False + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.is_fire_and_forget is True diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py new file mode 100644 index 000000000..7e68d9faa --- /dev/null +++ b/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py @@ -0,0 +1,69 @@ +import pytest +from pydantic import ValidationError + +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, PipelexPipeRunOutput +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + + +class TestInputOutputModels: + def test_input_defaults_match_design(self): + payload = PipelexPipeRunInput(pipe_code="some_pipe") + assert payload.pipe_code == "some_pipe" + assert payload.inputs == {} + assert payload.output_name is None + assert payload.pipeline_run_id is None + assert payload.user_id is None + assert payload.library_crate_dump is None + assert payload.execution_mode is PipelexExecutionMode.DIRECT + assert payload.delivery_assignment_dump is None + + def test_input_forbids_extra_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunInput.model_validate( + { + "pipe_code": "some_pipe", + "unexpected": "field", + } + ) + + def test_input_requires_pipe_code(self): + with pytest.raises(ValidationError): + PipelexPipeRunInput.model_validate({}) + + def test_input_round_trip_via_json(self): + original = PipelexPipeRunInput( + pipe_code="some_pipe", + inputs={"foo": "bar"}, + execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, + pipeline_run_id="run-123", + user_id="alice", + ) + round_tripped = PipelexPipeRunInput.model_validate(original.model_dump(mode="json")) + assert round_tripped == original + + def test_output_required_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunOutput.model_validate({"output_dict": {}}) # missing pipeline_run_id and is_completed + + def test_output_forbids_extra_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunOutput.model_validate( + { + "output_dict": {}, + "pipeline_run_id": "run-1", + "is_completed": True, + "rogue_field": 42, + } + ) + + def test_output_round_trip_via_json(self): + original = PipelexPipeRunOutput( + output_dict={"foo": "bar"}, + main_stuff_name="main", + pipeline_run_id="run-1", + workflow_id=None, + is_completed=True, + graph_spec_dump=None, + ) + round_tripped = PipelexPipeRunOutput.model_validate(original.model_dump(mode="json")) + assert round_tripped == original diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py new file mode 100644 index 000000000..ca59cdd08 --- /dev/null +++ b/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py @@ -0,0 +1,75 @@ +import pytest + +from pipelex.libraries.library_crate import LibraryCrate +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + _decode_delivery_assignment, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + _decode_library_crate, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + _validate_input, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.exceptions import PipelexBridgeRuntimeError +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + + +class TestBridgeValidationAndDecoding: + def test_validate_input_passes_for_direct_without_delivery(self): + payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.DIRECT) + _validate_input(payload) # must not raise + + def test_validate_input_passes_for_temporal_blocking_without_delivery(self): + payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING) + _validate_input(payload) # must not raise + + def test_validate_input_rejects_fire_and_forget_without_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + ) + with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): + _validate_input(payload) + + def test_validate_input_accepts_fire_and_forget_with_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump={"webhooks": [], "storage": None}, + ) + _validate_input(payload) # must not raise + + def test_decode_library_crate_returns_none_for_none(self): + assert _decode_library_crate(None) is None + + def test_decode_library_crate_round_trips_empty(self): + empty = LibraryCrate() + decoded = _decode_library_crate(empty.model_dump(mode="json")) + assert decoded is not None + assert decoded.concepts == empty.concepts + assert decoded.pipes == empty.pipes + + def test_decode_delivery_assignment_returns_none_for_none(self): + assert _decode_delivery_assignment(None) is None + + def test_decode_delivery_assignment_round_trips(self): + assignment = DeliveryAssignment.model_validate( + { + "storage": {"key_prefix": "runs/abc"}, + "webhooks": [{"url": "https://example.test/hook"}], + } + ) + decoded = _decode_delivery_assignment(assignment.model_dump(mode="json")) + assert decoded is not None + assert decoded.storage is not None + assert decoded.storage.key_prefix == "runs/abc/" # storage validator appends trailing / + assert len(decoded.webhooks) == 1 + assert decoded.webhooks[0].url == "https://example.test/hook" + + @pytest.mark.asyncio + async def test_run_pipe_via_bridge_rejects_fire_and_forget_without_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + ) + with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): + await run_pipe_via_bridge(payload) diff --git a/tests/unit/pipelex/plugins/test_plugin_pipelex_storage_images.py b/tests/unit/pipelex/plugins/test_plugin_pipelex_storage_images.py index 071305e91..0f511b487 100644 --- a/tests/unit/pipelex/plugins/test_plugin_pipelex_storage_images.py +++ b/tests/unit/pipelex/plugins/test_plugin_pipelex_storage_images.py @@ -6,7 +6,7 @@ import pytest import pytest_asyncio from google.genai import types as genai_types -from mistralai.models import ImageURLChunk +from mistralai.client.models import ImageURLChunk from pytest_mock import MockerFixture from pipelex.cogt.image.prompt_image import PromptImageUri diff --git a/uv.lock b/uv.lock index 64ca01331..daab350da 100644 --- a/uv.lock +++ b/uv.lock @@ -212,7 +212,7 @@ wheels = [ [[package]] name = "anthropic" -version = "0.99.0" +version = "0.100.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -224,9 +224,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/c9/e8a3a1caeab575e80551b30b084096b5a430abc52739a526a1daaadd038c/anthropic-0.99.0.tar.gz", hash = "sha256:16f41e00f215ed2d193b146be3dd567c4319c32ed3af6c8725d68ba875257c1c", size = 727239, upload-time = "2026-05-05T16:03:07.986Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/2d/24caf0ff727cba2ed863925017c8f93463a2ea6224a0efe5626e672bc3d2/anthropic-0.100.0.tar.gz", hash = "sha256:650dee9e023afb16395939ee4104bbc21f966b380210119fb91122c12099c79a", size = 758255, upload-time = "2026-05-06T15:07:13.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/84/d0917506744e1707cf55659a57f1e3ff952eda5636df0ffffe3e884b7c61/anthropic-0.99.0-py3-none-any.whl", hash = "sha256:c44469b746ab2ef19a4c52dcbdb98e17bc95c60bebdd18ec40d76d2d23592b49", size = 700564, upload-time = "2026-05-05T16:03:06.059Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/c775c59ab9445ecabb57ef3d5c24027de060139189a9e312ef9ef889a665/anthropic-0.100.0-py3-none-any.whl", hash = "sha256:1c15769efa15d8fd5c1ebf900e25c57e3ee540f8554a29aa56e4edefffe2951d", size = 753596, upload-time = "2026-05-06T15:07:12.106Z" }, ] [[package]] @@ -745,7 +745,7 @@ name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, @@ -776,37 +776,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] @@ -1505,7 +1505,7 @@ wheels = [ [[package]] name = "instructor" version = "1.15.1" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c#0efd9c09b05ef561defff3a8b86fe86e5e61214c" } dependencies = [ { name = "aiohttp" }, { name = "docstring-parser" }, @@ -1519,10 +1519,6 @@ dependencies = [ { name = "tenacity" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/a4/832cfb15420360e26d2d85bd9d5fe1e4b839d52587574d389bc31284bf6f/instructor-1.15.1.tar.gz", hash = "sha256:c72406469d9025b742e83cf0c13e914b317db2089d08d889944e74fcd659ef94", size = 69948370, upload-time = "2026-04-03T01:51:30.107Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/c8/36c5d9b80aaf40ba9a7084a8fc18c967db6bf248a4cc8d0f0816b14284be/instructor-1.15.1-py3-none-any.whl", hash = "sha256:be81d17ba2b154a04ab4720808f24f9d6b598f80992f82eaf9cc79006099cf6c", size = 178156, upload-time = "2026-04-03T01:51:23.098Z" }, -] [package.optional-dependencies] google-genai = [ @@ -1553,70 +1549,74 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -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/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/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" }, +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, ] [[package]] @@ -1897,14 +1897,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5c/f3aedc83549aae71cd52b9e9687fe896e3dc6e966ba20eba04718605d198/markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c", size = 81613, upload-time = "2026-05-06T16:32:13.649Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d", size = 90955, upload-time = "2026-05-06T16:32:12.184Z" }, ] [[package]] @@ -2602,7 +2602,7 @@ name = "nvidia-cudnn-cu13" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, @@ -2614,7 +2614,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -2644,9 +2644,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -2658,7 +2658,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -2734,7 +2734,7 @@ wheels = [ [[package]] name = "openai" -version = "2.34.0" +version = "2.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2746,9 +2746,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/89/f1e78f5f828f4e97a6ebca8f45c6b35667da12b074ac490dc8362b882279/openai-2.34.0.tar.gz", hash = "sha256:828b4efcbb126352c2b5eb97d33ae890c92a71ab72511aefc1b7fe64aeccb07b", size = 759556, upload-time = "2026-05-04T17:34:08.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/4c/35a5216fe5f1cd4d7002b037ba47cff10b71cbd4bddcb601262c664d08de/openai-2.35.0.tar.gz", hash = "sha256:607f62257d6be167240c6b82db052fabf940e3c4d9ad3e8629364e837a601395", size = 751972, upload-time = "2026-05-06T16:36:55.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/40/f090499f10514515081d09cb9da09f25b821eb20497e9423afe4f07b4ecf/openai-2.34.0-py3-none-any.whl", hash = "sha256:c996a71b1a210f3569844572ad4c609307e978515fb76877cf449b72596e549e", size = 1316535, upload-time = "2026-05-04T17:34:06.773Z" }, + { url = "https://files.pythonhosted.org/packages/58/b7/c43595f7f441cbc62ac3144a080d71952566b213f7a21bca0564d69e39fd/openai-2.35.0-py3-none-any.whl", hash = "sha256:164fd0477d001e784369f7cd81ccadb8db3c22f16b33973d8f95e3095c7f71d8", size = 1300139, upload-time = "2026-05-06T16:36:53.108Z" }, ] [[package]] @@ -3266,8 +3266,8 @@ requires-dist = [ { name = "google-genai", marker = "extra == 'google-genai'" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "huggingface-hub", marker = "extra == 'huggingface'", specifier = ">=0.23,<1.0.0" }, - { name = "instructor", specifier = ">=1.8.3,!=1.11.*,!=1.12.*" }, - { name = "instructor", extras = ["google-genai"], marker = "extra == 'google-genai'" }, + { name = "instructor", git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c" }, + { name = "instructor", extras = ["google-genai"], marker = "extra == 'google-genai'", git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c" }, { name = "jinja2", specifier = ">=3.1.4" }, { name = "json2html", specifier = ">=1.3.0" }, { name = "kajson", specifier = "==0.5.0" }, @@ -3275,7 +3275,7 @@ requires-dist = [ { name = "lxml", marker = "extra == 'docling'", specifier = ">=6.1.0" }, { name = "markdown", specifier = ">=3.6" }, { name = "mike", marker = "extra == 'docs'", specifier = ">=2.1.3" }, - { name = "mistralai", marker = "extra == 'mistralai'", specifier = ">=1.12.0" }, + { name = "mistralai", marker = "extra == 'mistralai'", specifier = ">=2.4.4" }, { name = "mistralai-workflows", marker = "extra == 'mistralai-workflows'", specifier = ">=3.3.0" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.1" }, { name = "mkdocs-glightbox", marker = "extra == 'docs'", specifier = ">=0.4.0" }, From 2032228bb7dc0bb0281b823126162d1f3292227b Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 00:45:11 +0200 Subject: [PATCH 03/16] Enhance Mistral Workflows integration with Pipelex - Introduced the `pipelex.plugins.mistralai_workflows` plugin, enabling invocation of Pipelex pipes within Mistral Workflows activities. - Added three execution modes: `DIRECT`, `TEMPORAL_BLOCKING`, and `TEMPORAL_FIRE_AND_FORGET`, allowing flexible integration options. - Created documentation for the new plugin and usage recipes, detailing installation and execution modes. - Implemented integration tests for both blocking and fire-and-forget modes to ensure functionality. - Updated `CHANGELOG.md` to reflect these additions and improvements. --- CHANGELOG.md | 6 + TODOS.md | 98 ++++++---- .../mistralai-workflows-plugin.md | 148 +++++++++++++++ .../mistralai-workflows-recipes.md | 170 ++++++++++++++++++ mkdocs.yml | 4 + .../test_bridge_temporal_blocking.py | 149 +++++++++++++++ .../test_bridge_temporal_fire_and_forget.py | 145 +++++++++++++++ .../test_data/bridge_test.mthds | 9 +- 8 files changed, 695 insertions(+), 34 deletions(-) create mode 100644 docs/under-the-hood/mistralai-workflows-plugin.md create mode 100644 docs/under-the-hood/mistralai-workflows-recipes.md create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c24c1469..7c1a82ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. + ## [v0.26.4] - 2026-05-06 ### Fixed diff --git a/TODOS.md b/TODOS.md index e8dcf0656..fc3e99875 100644 --- a/TODOS.md +++ b/TODOS.md @@ -572,46 +572,78 @@ lazy-imports `temporalio` only inside the temporal-mode branches. `follow_imports = "skip"` and `ignore_errors = true` for `mistralai.workflows.*`. -### Phase 1.2 — Temporal modes +### Phase 1.2 — Temporal modes — **DONE** -- [ ] Wire `_run_temporal_blocking` to call `make_temporal_pipe_run()` and - await `.run(pipe_job, delivery_assignment)` -- [ ] Wire `_run_temporal_fire_and_forget` to call - `make_temporal_pipe_run().start(...)`, return immediately with +Wiring was already in place in `bridge.py` from Phase 1.0; this phase added +the layer-3 integration tests against a real Pipelex Temporal worker. + +- [x] `_run_temporal_blocking` wired to `make_temporal_pipe_run().run(...)` +- [x] `_run_temporal_fire_and_forget` wired to + `make_temporal_pipe_run().start(...)` — returns immediately with `workflow_id` and `is_completed=False` -- [ ] Layer-3 integration test (`test_activities_temporal_blocking.py`): - - [ ] `pytest.importorskip("temporalio")` at module level - - [ ] Boot Pipelex with `temporal.is_enabled=true` against the test - Temporal env - - [ ] Run a Mistral activity that dispatches a Pipelex `WfPipeRun` - and blocks on the result - - [ ] Assert end-to-end output equality with a DIRECT-mode reference - run of the same pipe -- [ ] Layer-3 fire-and-forget test: - - [ ] Mock `DeliveryExecutor` / webhook target - - [ ] Verify activity returns immediately with non-None - `workflow_id` and `is_completed=False` - - [ ] Verify the Pipelex workflow eventually completes and posts to - the delivery target +- [x] Layer-3 integration test + (`test_bridge_temporal_blocking.py`): + - [x] `pytest.importorskip("temporalio")` at module level + - [x] Boots Pipelex's Temporal layer (`TemporalTaskManager` + + `make_temporal_pipe_router` + `ContentGeneratorChild`) and + pre-connects `TemporalManager` to the test env's client + - [x] Calls `run_pipe_via_bridge` (Tier-3 entry point) in + TEMPORAL_BLOCKING mode — same code path the Mistral activity + wrapper uses, just without the single-line `@activity` + decoration (which Layer 2 already validates for DIRECT mode). + - [x] Asserts `is_completed=True`, `workflow_id` is set, and the + pipe's output round-trips through `WfPipeRouter` +- [x] Layer-3 fire-and-forget test + (`test_bridge_temporal_fire_and_forget.py`): + - [x] Bridge returns immediately with non-None `workflow_id` and + `is_completed=False`; `output_dict={}`, `main_stuff_name=None` + - [x] Verifies the Pipelex workflow eventually completes + (`WorkflowExecutionStatus.COMPLETED` after `handle.result()`) + - [x] Empty `DeliveryAssignment()` (no storage, no webhooks) — + satisfies `_validate_input` without depending on external + webhook infrastructure + +**Notes & follow-ups**: + +- `bridge_test.mthds` got a second pipe `bridge_compose_pipe` (PipeCompose + with a Jinja2 template). This pipe is Temporal-compatible, whereas + `bridge_func_pipe` (PipeFunc) is not — `asyncio.to_thread` inside + `PipeFunc` raises `NotImplementedError` in the deterministic workflow + event loop. Tests pick the appropriate pipe per mode. +- A wider layer-3 test that wraps the bridge in the Mistral activity + (`@activity`-decorated `pipelex_run_pipe`) and dispatches both the + Mistral workflow and the Pipelex Temporal workflow on the same in-process + Temporal server is feasible but heavier (parallel Mistral test worker + + Pipelex worker + cross-converter compatibility checks). The Mistral + activity wrapping is a single-line `@activity` decoration over + `run_pipe_via_bridge` already validated end-to-end for DIRECT mode in + `test_activities_direct.py`, so no new branching logic is exercised by + re-running the same wrapping for TEMPORAL_*. Deferred unless we add + TEMPORAL_*-specific behavior to the activity layer. ### Phase 1.3 — Docs, changelog, CI matrix -- [ ] Write `docs/under-the-hood/mistralai-workflows-plugin.md` (overview + - install + when to use which `PipelexExecutionMode`) -- [ ] Write `docs/under-the-hood/mistralai-workflows-recipes.md` with - worked examples: Tier 1, Tier 2, library_crate -- [ ] Update `CHANGELOG.md` Unreleased: "Added: Pipelex pipes can now be +- [x] Write `docs/under-the-hood/mistralai-workflows-plugin.md` (overview + + install + when to use which `PipelexExecutionMode`) and wire it into + `mkdocs.yml` +- [x] Write `docs/under-the-hood/mistralai-workflows-recipes.md` with + worked examples: Tier 1, Tier 2, library_crate (including + `TEMPORAL_FIRE_AND_FORGET` with a `DeliveryAssignment` example) +- [x] Update `CHANGELOG.md` Unreleased: "Added: Pipelex pipes can now be invoked from inside Mistral Workflows activities via the new `pipelex.plugins.mistralai_workflows` plugin." -- [ ] CI matrix: - - [ ] `unit` lane: `pip install -e .[dev]` — runs layer 1 - - [ ] `mistralai-workflows` lane: - `pip install -e .[dev,mistralai-workflows]` — adds layer 2 - - [ ] `mistralai-workflows-temporal` lane: - `pip install -e .[dev,mistralai-workflows,temporal]` — adds - layer 3 -- [ ] Add a starter example to `pipelex-cookbook/` under a new - `mistral-workflows/` directory +- [ ] CI matrix — **deferred**: the existing CI (`tests-check.yml`) runs + `make install` which calls `uv sync --all-extras`, so layer 2 and 3 + tests already run on every PR. Pipelex doesn't currently maintain + separate per-extras CI lanes for any other optional dep; introducing + the convention solely for `mistralai-workflows` is out of scope. + Reconsider when we add an extra whose tests must NOT run on the + default lane. +- [ ] Cookbook example — **deferred to a follow-up PR in + `pipelex-cookbook/`**. That repo is a sibling, not part of this + worktree. Suggested entry: `examples/c_advanced/mistral-workflows/` + with a Tier-1 DIRECT-mode worker + a Tier-2 typed activity using + `library_crate_dump`. ### Phase 1.5 — Large payload offloading diff --git a/docs/under-the-hood/mistralai-workflows-plugin.md b/docs/under-the-hood/mistralai-workflows-plugin.md new file mode 100644 index 000000000..a06a876c8 --- /dev/null +++ b/docs/under-the-hood/mistralai-workflows-plugin.md @@ -0,0 +1,148 @@ +--- +title: "Mistral Workflows Plugin" +description: "Run Pipelex pipes from inside Mistral Workflows activities — install, execution modes, and when to pick which." +--- + +# Mistral Workflows Plugin + +The `pipelex.plugins.mistralai_workflows` plugin lets you call Pipelex pipes from inside [Mistral Workflows](https://docs.mistral.ai/) activities. Pipelex remains in charge of pipe orchestration; Mistral Workflows owns the surrounding activity, retry policy, scheduling, and (optionally) durable execution. + +For worked examples (Tier 1 pre-decorated activity, Tier 2 helper-in-your-own-activity, Tier 3 full control with `library_crate_dump`), see the [Recipes](./mistralai-workflows-recipes.md) page. + +--- + +## Install + +The `mistralai-workflows` dependency is **strictly optional**. Install it as an extra: + +```bash +pip install 'pipelex[mistralai-workflows]' +``` + +For the `TEMPORAL_BLOCKING` and `TEMPORAL_FIRE_AND_FORGET` execution modes, also install the `temporal` extra: + +```bash +pip install 'pipelex[mistralai-workflows,temporal]' +``` + +The framework-agnostic core (`bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`) is importable on a venv that does NOT have `mistralai-workflows` installed. The optional-dep guard fires only when you import `pipelex.plugins.mistralai_workflows.activities` (or `streaming` once shipped). + +--- + +## What you can import + +Per Pipelex's no-re-exports rule, import from the full path: + +```python +from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.plugins.mistralai_workflows.bootstrap import ( + ensure_pipelex_booted, + get_pipelex_dependency, +) +``` + +--- + +## Execution modes + +`PipelexExecutionMode` is set per-call via `PipelexPipeRunInput.execution_mode`. It is exhaustive: any new mode added later will surface as a linting error in every `match` statement that consumes it. + +### `DIRECT` + +The pipe runs in-process inside the Mistral activity. No Temporal involvement on Pipelex's side. The activity blocks until the pipe completes. + +- **When to use:** simple integrations, fast feedback, tests, environments without a Pipelex Temporal worker. +- **Requires:** `pipelex[mistralai-workflows]`. + +### `TEMPORAL_BLOCKING` + +The bridge dispatches the pipe as a Pipelex Temporal workflow (`WfPipeRun`) and awaits completion. The pipe runs durably on the Pipelex worker fleet; the Mistral activity blocks until that workflow returns. + +- **When to use:** you already operate a Pipelex Temporal cluster and want pipes to run durably with Pipelex's existing observability and retry semantics. +- **Requires:** `pipelex[mistralai-workflows,temporal]`. + +### `TEMPORAL_FIRE_AND_FORGET` + +The bridge dispatches the pipe as a Pipelex Temporal workflow and returns immediately with the workflow id. The activity does NOT wait. Completion is delivered out-of-band via a `DeliveryAssignment` (storage and/or webhook). + +- **When to use:** long-running pipes (multi-minute LLM jobs, large extractions) where you don't want the surrounding Mistral activity to keep its slot for the full duration. +- **Requires:** `pipelex[mistralai-workflows,temporal]`. +- **Validation:** `delivery_assignment_dump` must be set; otherwise `run_pipe_via_bridge` raises `PipelexBridgeRuntimeError` to prevent silently-dropped completions. + +--- + +## Boundary types + +Everything that crosses the Mistral/Temporal boundary is JSON-only: + +- `PipelexPipeRunInput.inputs` — `dict[str, Any]` +- `PipelexPipeRunInput.library_crate_dump` — `dict[str, Any] | None` (a `LibraryCrate.model_dump(mode="json")`) +- `PipelexPipeRunInput.delivery_assignment_dump` — `dict[str, Any] | None` (a `DeliveryAssignment.model_dump(mode="json")`) +- `PipelexPipeRunOutput.output_dict` — `dict[str, Any]` produced by `WorkingMemory.dump_for_temporal()` +- `PipelexPipeRunOutput.graph_spec_dump` — `dict[str, Any] | None` + +No internal Pipelex types (`PipeJob`, `PipeOutput`, `WorkingMemory`) cross the activity boundary. The bridge serializes via `WorkingMemory.dump_for_temporal()` regardless of execution mode, so the `output_dict` shape is stable. + +--- + +## Bootstrapping + +Boot Pipelex once before the Mistral worker starts; the activity is then a thin wrapper around `run_pipe_via_bridge`: + +```python +import asyncio +from mistralai import workflows + +from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted + + +async def main() -> None: + ensure_pipelex_booted() + await workflows.run_worker([MyFlow], activities=[pipelex_run_pipe]) + + +asyncio.run(main()) +``` + +`ensure_pipelex_booted()` is idempotent and safe to call from inside the activity too — useful for tests or first-run safety nets — but in production it should be called explicitly at worker startup so Pipelex initialization is not on the critical path of the first activity. + +--- + +## Per-call library scoping (`library_crate_dump`) + +When a `library_crate_dump` is provided on the input, the bridge opens a per-call scoped library, loads the crate, runs the pipe inside that scope, and tears down on the way out. The global registry is left untouched. + +This is the same scoping mechanism Pipelex's own Temporal layer uses (`pipelex/temporal/tprl_pipe/wf_pipe_router.py`) and is the recommended way to invoke a pipe whose bundle is not pre-loaded into the worker's global registry — for example, when the calling activity received the bundle as part of an API request. + +--- + +## Error mapping + +The bridge maps Pipelex execution errors into a single `PipelexBridgeRuntimeError` chained from the original exception. Mistral / Temporal infrastructure errors (connection, dispatch) propagate unchanged. + +| Exception | When | +| -------------------------------------- | ----------------------------------------------------------------------------- | +| `MistralWorkflowsNotInstalledError` | Importing `activities` (or `streaming`) without the optional dep installed | +| `MissingPipelexTemporalExtraError` | Calling `TEMPORAL_*` modes without `pipelex[temporal]` installed | +| `PipelexBridgeRuntimeError` | Pipe execution failed; original exception is on `__cause__` | +| `MistralWorkflowsPluginError` | Common base for plugin-specific errors | + +--- + +## Boundary semantics summary + +| Aspect | DIRECT | TEMPORAL_BLOCKING | TEMPORAL_FIRE_AND_FORGET | +| ----------------------- | --------------------- | -------------------------------- | ------------------------------------ | +| Pipelex worker required | No | Yes | Yes | +| Activity blocks | Yes | Yes (until WfPipeRun completes) | No (returns workflow_id immediately) | +| `is_completed` returned | `True` | `True` | `False` | +| `workflow_id` returned | `None` | Pipelex Temporal workflow id | Pipelex Temporal workflow id | +| `output_dict` populated | Yes | Yes | `{}` | +| Completion delivery | In-band | In-band | Out-of-band via `DeliveryAssignment` | diff --git a/docs/under-the-hood/mistralai-workflows-recipes.md b/docs/under-the-hood/mistralai-workflows-recipes.md new file mode 100644 index 000000000..896d9ba08 --- /dev/null +++ b/docs/under-the-hood/mistralai-workflows-recipes.md @@ -0,0 +1,170 @@ +--- +title: "Mistral Workflows Recipes" +description: "Three integration tiers for invoking Pipelex pipes from Mistral Workflows activities — pre-decorated, helper-in-your-own-activity, full control." +--- + +# Mistral Workflows Recipes + +For the architecture and execution-mode reference, see the [plugin overview](./mistralai-workflows-plugin.md). + +The plugin offers three usage tiers, in order of decreasing convenience and increasing control. Pick the tier that matches how much customization you need around the activity itself. + +--- + +## Tier 1 — pre-decorated activity (the fast path) + +Use the ready-made `pipelex_run_pipe` activity directly. Nothing to configure beyond pipe code and inputs. + +```python +import asyncio +from mistralai import workflows + +from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + + +@workflows.workflow.define(name="extract-invoice-flow") +class ExtractInvoiceFlow: + @workflows.workflow.entrypoint + async def run(self, doc_url: str) -> dict: + result = await pipelex_run_pipe( + PipelexPipeRunInput( + pipe_code="finance.extract_invoice", + inputs={"doc_url": doc_url}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + return result.output_dict + + +async def main() -> None: + ensure_pipelex_booted() + await workflows.run_worker([ExtractInvoiceFlow], activities=[pipelex_run_pipe]) + + +asyncio.run(main()) +``` + +The activity has sensible defaults (10 minute timeout, 3 retries). When you need different timeouts, retry policies, rate limits, or sticky-to-worker behavior — go to Tier 2. + +--- + +## Tier 2 — helper inside your own typed activity + +Wrap `run_pipe_via_bridge` in your own `@activity`-decorated function so you control all activity options and the input/output types. + +```python +from datetime import timedelta + +from mistralai import workflows +from pydantic import BaseModel + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + run_pipe_via_bridge, +) + + +class InvoiceData(BaseModel): + invoice_number: str + total_amount: float + currency: str + + +@workflows.activity( + start_to_close_timeout=timedelta(minutes=30), + retry_policy_max_attempts=5, +) +async def extract_invoice(doc_url: str) -> InvoiceData: + out = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="finance.extract_invoice", + inputs={"doc_url": doc_url}, + ) + ) + main_stuff = out.output_dict["root"][out.main_stuff_name] + return InvoiceData.model_validate(main_stuff["content"]) +``` + +The `run_pipe_via_bridge` helper is the same code the Tier 1 activity calls — just without the decoration. This is the recommended tier for production: you keep typed activity inputs/outputs, custom retries per pipe, and you can register multiple pipe-specific activities (`extract_invoice`, `summarize_contract`, ...) on the same Mistral worker. + +--- + +## Tier 3 — full control (`library_crate_dump`) + +Tier 3 is for cases where the Pipelex bundle is not pre-loaded into the worker's global registry — for example, the calling Mistral workflow received the bundle as part of an API request and needs to run a pipe defined in it without polluting the shared library. + +```python +from mistralai import workflows + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + run_pipe_via_bridge, +) + + +@workflows.activity() +async def run_user_supplied_pipe( + pipe_code: str, + inputs: dict, + library_crate_dump: dict, +) -> dict: + out = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=pipe_code, + inputs=inputs, + library_crate_dump=library_crate_dump, + ) + ) + return out.output_dict +``` + +The bridge opens a per-call scoped library, loads the crate, runs the pipe, and tears the scope down on the way out. The global registry is untouched — concurrent activities with different `library_crate_dump`s do not see each other's classes. + +To produce the dump on the submitter side: + +```python +from pipelex.hub import get_library_manager + +crate = get_library_manager().get_crate(library_id=my_lib_id) +crate_dump = crate.model_dump(mode="json") +``` + +--- + +## Picking an execution mode + +| You want… | Use | +| -------------------------------------------------------------------- | ---------------------------- | +| Run a pipe in-process inside the Mistral activity | `DIRECT` | +| Hand off pipe execution to your existing Pipelex Temporal cluster | `TEMPORAL_BLOCKING` | +| Don't block the activity for a long-running pipe; deliver out-of-band | `TEMPORAL_FIRE_AND_FORGET` | + +`TEMPORAL_FIRE_AND_FORGET` requires `delivery_assignment_dump` so the completion can reach somebody — webhook, storage target, or both. + +```python +from pipelex.pipe_run.delivery_assignment import ( + DeliveryAssignment, + StorageTarget, + WebhookTarget, +) + +delivery = DeliveryAssignment( + storage=StorageTarget(key_prefix="invoices/2026/"), + webhooks=[WebhookTarget(url="https://my.app/pipelex-callback")], +) + +result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="finance.extract_invoice", + inputs={"doc_url": doc_url}, + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump=delivery.model_dump(mode="json"), + ) +) +# result.is_completed is False +# result.workflow_id is the Pipelex Temporal workflow id +# Completion arrives at the webhook + storage location later. +``` diff --git a/mkdocs.yml b/mkdocs.yml index 1ce9f4528..a5e51a222 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -307,6 +307,8 @@ plugins: - under-the-hood/pipe-routing-and-execution.md: "Pipe Routing & Execution" - under-the-hood/temporal-integration.md: "Temporal Integration" - under-the-hood/distributed-content-generation.md: "Distributed Content Generation" + - under-the-hood/mistralai-workflows-plugin.md: "Mistral Workflows Plugin" + - under-the-hood/mistralai-workflows-recipes.md: "Mistral Workflows Recipes" Project: - contributing.md: "Contributing" - contribute/configuration-defaults-and-overrides.md: "Configuration Internals" @@ -495,6 +497,8 @@ nav: - Pipe Routing & Execution: under-the-hood/pipe-routing-and-execution.md - Temporal Integration: under-the-hood/temporal-integration.md - Distributed Content Generation: under-the-hood/distributed-content-generation.md + - Mistral Workflows Plugin: under-the-hood/mistralai-workflows-plugin.md + - Mistral Workflows Recipes: under-the-hood/mistralai-workflows-recipes.md - Project: - Contributing: contributing.md - Configuration Internals: contribute/configuration-defaults-and-overrides.md diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py new file mode 100644 index 000000000..bc75c9bfc --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py @@ -0,0 +1,149 @@ +"""Layer-3 integration test: ``run_pipe_via_bridge`` end-to-end in TEMPORAL_BLOCKING mode. + +The bridge dispatches a Pipelex ``WfPipeRun`` workflow through the same +``make_temporal_pipe_run`` helper that the activity wrapper uses, and waits +for completion. This validates the full bridge → Pipelex Temporal wiring. + +The Mistral ``@activity`` wrapping over ``run_pipe_via_bridge`` is a +single-line decoration already validated end-to-end in DIRECT mode by +``test_activities_direct.py``; the same wrapping flows through this code path +unchanged. + +Skipped when ``temporalio`` (or ``mistralai-workflows``) is not installed. +""" + +from collections.abc import AsyncGenerator, Generator + +import pytest +import pytest_asyncio + +pytest.importorskip("temporalio") +pytest.importorskip("mistralai.workflows") + +from temporalio.testing import WorkflowEnvironment + +from pipelex.cogt.content_generation.generated_content_factory import GeneratedContentFactory +from pipelex.config import get_config +from pipelex.hub import get_pipelex_hub, get_storage_provider +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.temporal.tasks import Tasks +from pipelex.temporal.temporal_data_converter import data_converter +from pipelex.temporal.temporal_hub import get_task_manager, temporal_hub +from pipelex.temporal.temporal_manager import TemporalManager, get_temporal_manager +from pipelex.temporal.temporal_task_manager import TemporalTaskManager +from pipelex.temporal.tprl_content_generation.content_generator_child_factory import ContentGeneratorChildFactory +from pipelex.temporal.tprl_pipe.temporal_pipe_router import make_temporal_pipe_router + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_compose_pipe" +TEST_TASK_QUEUE = "pipelex-bridge-temporal-blocking-test" + + +@pytest.fixture(scope="module", autouse=True) +def _enable_pipelex_temporal_for_bridge() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] + """Enable Pipelex Temporal and pin the worker task_queue to our test queue. + + The bridge calls ``make_temporal_pipe_run()`` with no arguments, which + reads the task_queue from ``get_config().temporal.worker_config.task_queue``. + """ + config = get_config() + original_is_enabled = config.temporal.is_enabled + original_task_queue = config.temporal.worker_config.task_queue + config.temporal.is_enabled = True + config.temporal.worker_config.task_queue = TEST_TASK_QUEUE + try: + yield + finally: + config.temporal.is_enabled = original_is_enabled + config.temporal.worker_config.task_queue = original_task_queue + + +@pytest.fixture(scope="module", autouse=True) +def _boot_pipelex_temporal_layer(_enable_pipelex_temporal_for_bridge: None) -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] + """Set up the Pipelex Temporal task manager + temporal-aware routers. + + Mirrors the production boot path: registers WfPipeRun / WfPipeRouter and + swaps the pipe_router and content_generator on the hub for their + Temporal-aware variants. Without this, dispatching ``WfPipeRun`` would + fail because the worker would have no workflows to register. + """ + manager = TemporalTaskManager() + temporal_hub.set_task_manager(manager) + manager.complement_catalog( + extra_catalog=Tasks.TASK_PACKS, + extra_workflows=[], + extra_activities=[], + ) + manager.setup() + + pipelex_hub = get_pipelex_hub() + original_pipe_router = pipelex_hub.get_required_pipe_router() + original_content_generator = pipelex_hub.get_required_content_generator() + + pipelex_hub.set_pipe_router(make_temporal_pipe_router()) + generated_content_factory = GeneratedContentFactory(storage_provider=get_storage_provider()) + pipelex_hub.set_content_generator( + ContentGeneratorChildFactory.make_content_generator_child( + generated_content_factory=generated_content_factory, + ) + ) + + TemporalManager.setup(session_id="bridge-temporal-blocking-test") + + try: + yield + finally: + TemporalManager.teardown() + pipelex_hub.set_pipe_router(original_pipe_router) + pipelex_hub.set_content_generator(original_content_generator) + manager.teardown() + temporal_hub.reset() + + +@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] +async def workflow_env() -> AsyncGenerator[WorkflowEnvironment, None]: + """Local Temporal env wired with Pipelex's data converter. + + Pre-connects ``TemporalManager`` to ``env.client`` so that + ``make_temporal_pipe_run()`` (called by the bridge with default + ``should_auto_connect_temporal=True``) reuses the same client instead of + auto-connecting to a non-existent production server. + """ + env = await WorkflowEnvironment.start_local(data_converter=data_converter) # pyright: ignore[reportUnknownMemberType] + try: + await get_temporal_manager().connect_temporal(temporal_client=env.client) + yield env + finally: + await env.shutdown() + + +@pytest.mark.temporal +@pytest.mark.asyncio(loop_scope="class") +class TestBridgeTemporalBlocking: + async def test_temporal_blocking_mode_end_to_end( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Bridge dispatches WfPipeRun on the test Temporal server and blocks until completion.""" + async with get_task_manager().make_worker( + temporal_client=workflow_env.client, + task_queue=TEST_TASK_QUEUE, + is_not_sandboxed=True, + ): + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "hello via temporal blocking"}, + execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, + ) + ) + + assert result.is_completed is True + assert result.workflow_id is not None + assert result.main_stuff_name is not None + main_stuff_dump = result.output_dict["root"][result.main_stuff_name] + assert main_stuff_dump["content"]["text"] == "hello via temporal blocking" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py new file mode 100644 index 000000000..9398bca24 --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py @@ -0,0 +1,145 @@ +"""Layer-3 integration test: ``run_pipe_via_bridge`` in TEMPORAL_FIRE_AND_FORGET mode. + +Validates that the bridge dispatches a Pipelex ``WfPipeRun`` workflow on the +test Temporal server and returns immediately without waiting for completion, +and that the workflow eventually completes asynchronously. + +Skipped when ``temporalio`` (or ``mistralai-workflows``) is not installed. +""" + +from collections.abc import AsyncGenerator, Generator + +import pytest +import pytest_asyncio + +pytest.importorskip("temporalio") +pytest.importorskip("mistralai.workflows") + +from temporalio.client import WorkflowExecutionStatus +from temporalio.testing import WorkflowEnvironment + +from pipelex.cogt.content_generation.generated_content_factory import GeneratedContentFactory +from pipelex.config import get_config +from pipelex.hub import get_pipelex_hub, get_storage_provider +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.temporal.tasks import Tasks +from pipelex.temporal.temporal_data_converter import data_converter +from pipelex.temporal.temporal_hub import get_task_manager, temporal_hub +from pipelex.temporal.temporal_manager import TemporalManager, get_temporal_manager +from pipelex.temporal.temporal_task_manager import TemporalTaskManager +from pipelex.temporal.tprl_content_generation.content_generator_child_factory import ContentGeneratorChildFactory +from pipelex.temporal.tprl_pipe.temporal_pipe_router import make_temporal_pipe_router + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_compose_pipe" +TEST_TASK_QUEUE = "pipelex-bridge-temporal-fire-and-forget-test" + + +@pytest.fixture(scope="module", autouse=True) +def _enable_pipelex_temporal_for_bridge() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] + config = get_config() + original_is_enabled = config.temporal.is_enabled + original_task_queue = config.temporal.worker_config.task_queue + config.temporal.is_enabled = True + config.temporal.worker_config.task_queue = TEST_TASK_QUEUE + try: + yield + finally: + config.temporal.is_enabled = original_is_enabled + config.temporal.worker_config.task_queue = original_task_queue + + +@pytest.fixture(scope="module", autouse=True) +def _boot_pipelex_temporal_layer(_enable_pipelex_temporal_for_bridge: None) -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] + """Mirrors the production boot path for Pipelex's Temporal layer.""" + manager = TemporalTaskManager() + temporal_hub.set_task_manager(manager) + manager.complement_catalog( + extra_catalog=Tasks.TASK_PACKS, + extra_workflows=[], + extra_activities=[], + ) + manager.setup() + + pipelex_hub = get_pipelex_hub() + original_pipe_router = pipelex_hub.get_required_pipe_router() + original_content_generator = pipelex_hub.get_required_content_generator() + + pipelex_hub.set_pipe_router(make_temporal_pipe_router()) + generated_content_factory = GeneratedContentFactory(storage_provider=get_storage_provider()) + pipelex_hub.set_content_generator( + ContentGeneratorChildFactory.make_content_generator_child( + generated_content_factory=generated_content_factory, + ) + ) + + TemporalManager.setup(session_id="bridge-temporal-faf-test") + + try: + yield + finally: + TemporalManager.teardown() + pipelex_hub.set_pipe_router(original_pipe_router) + pipelex_hub.set_content_generator(original_content_generator) + manager.teardown() + temporal_hub.reset() + + +@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] +async def workflow_env() -> AsyncGenerator[WorkflowEnvironment, None]: + env = await WorkflowEnvironment.start_local(data_converter=data_converter) # pyright: ignore[reportUnknownMemberType] + try: + await get_temporal_manager().connect_temporal(temporal_client=env.client) + yield env + finally: + await env.shutdown() + + +@pytest.mark.temporal +@pytest.mark.asyncio(loop_scope="class") +class TestBridgeTemporalFireAndForget: + async def test_fire_and_forget_returns_immediately_and_workflow_completes( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Bridge starts WfPipeRun without waiting; the workflow completes asynchronously. + + Asserts in two phases: + + 1. The bridge returns with ``is_completed=False`` and a non-None + ``workflow_id`` — proving the dispatch did not block. + 2. The Pipelex Temporal workflow eventually completes with + ``COMPLETED`` status when the worker is given time to run. + """ + delivery_assignment_dump = DeliveryAssignment().model_dump(mode="json") + + async with get_task_manager().make_worker( + temporal_client=workflow_env.client, + task_queue=TEST_TASK_QUEUE, + is_not_sandboxed=True, + ): + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "hello via temporal fire and forget"}, + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump=delivery_assignment_dump, + ) + ) + + # Phase 1 — dispatch returned immediately without waiting. + assert result.is_completed is False + assert result.workflow_id is not None + assert result.output_dict == {} + assert result.main_stuff_name is None + + # Phase 2 — the Pipelex workflow eventually completes on the worker. + handle = workflow_env.client.get_workflow_handle(workflow_id=result.workflow_id) + await handle.result() + description = await handle.describe() + assert description.status == WorkflowExecutionStatus.COMPLETED diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds index bf6b02f21..656f83946 100644 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds @@ -3,6 +3,13 @@ description = "Test pipes for the mistralai_workflows plugin bridge" [pipe.bridge_func_pipe] type = "PipeFunc" -description = "Echoes the input text back as output" +description = "Echoes the input text back as output (DIRECT mode only — PipeFunc is not Temporal-compatible)" output = "Text" function_name = "mistralai_workflows_bridge_echo" + +[pipe.bridge_compose_pipe] +type = "PipeCompose" +description = "Echoes the input_text via a Jinja2 template (Temporal-compatible)" +inputs = { input_text = "Text" } +output = "Text" +template = "{{ input_text.text }}" From 1f2f9b5ca4e90094cd8d67ab7c5bca5b69144df8 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 01:00:03 +0200 Subject: [PATCH 04/16] Enhance Pipelex integration with Mistral Workflows - Introduced `pipelex_run_pipe_offloaded` activity to handle large payloads by leveraging Mistral's `ActivityInOutOffloadingInterceptor`, allowing seamless streaming of oversized data through blob storage. - Updated `CHANGELOG.md` to document the new offloaded activity and its usage. - Added integration tests for the offloaded activity to ensure correct payload handling and functionality. - Enhanced documentation with examples for using the new offloaded activity and its configuration requirements. --- CHANGELOG.md | 2 +- TODOS.md | 934 +++++------------- .../mistralai-workflows-recipes.md | 56 ++ .../plugins/mistralai_workflows/activities.py | 54 + .../test_activities_offloaded.py | 117 +++ .../mistralai_workflows/test_bridge_direct.py | 33 + .../test_data/bridge_test.mthds | 17 + 7 files changed, 500 insertions(+), 713 deletions(-) create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c1a82ad6..d99c950dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. +- **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. A second activity `pipelex_run_pipe_offloaded` (with `PipelexPipeRunInputOffloaded` / `PipelexPipeRunOutputOffloaded` boundary types wrapped in `OffloadableField`) lets users plug into Mistral's `ActivityInOutOffloadingInterceptor` for payloads that exceed Temporal's per-event size limit. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. ## [v0.26.4] - 2026-05-06 diff --git a/TODOS.md b/TODOS.md index fc3e99875..6440a3fe0 100644 --- a/TODOS.md +++ b/TODOS.md @@ -3,760 +3,270 @@ Self-contained planning document. A fresh session can resume from this file alone; no need to read prior conversation history. +For anything implemented (Phases 1.0–1.5), the code is authoritative. Read +`pipelex/plugins/mistralai_workflows/` and the matching tests under +`tests/{unit,integration}/pipelex/plugins/mistralai_workflows/`. The +user-facing reference lives at +`docs/under-the-hood/mistralai-workflows-{plugin,recipes}.md`. + --- -## 1. Context +## Status board + +| Phase | Scope | Status | +| ----- | ------------------------------------------------------ | ------ | +| 1.0 | Framework-agnostic core (`bridge.py`, modes, ...) | ✅ done | +| 1.1 | Tier-1 `pipelex_run_pipe` activity wrapper | ✅ done | +| 1.2 | `TEMPORAL_BLOCKING` + `TEMPORAL_FIRE_AND_FORGET` modes | ✅ done | +| 1.3 | Docs + CHANGELOG (mkdocs nav wired) | ✅ done¹ | +| 1.5 | Large-payload `pipelex_run_pipe_offloaded` variant | ✅ done | +| 2.0 | Streaming v1 — one Mistral `task()` per activity | ⏳ next | +| 2.1 | Streaming v2 — per-step `task.update()` (conditional) | ⏳ after 2.0 | + +¹ Cookbook example deferred to a follow-up PR in the sibling +`pipelex-cookbook/` repo (suggested entry: +`examples/c_advanced/mistral-workflows/` with a Tier-1 DIRECT-mode worker +plus a Tier-2 typed activity that uses `library_crate_dump`). + +### Outstanding from Phase 1 (not blocking Phase 2) + +- [ ] `make agent-check` passes with `mistralai-workflows` NOT installed — + needs a fresh venv without the extra. The guard placement + (`activities.py` only; `bridge.py` / `execution_mode.py` / + `bootstrap.py` / `exceptions.py` are import-clean) should make this + pass; not yet verified end-to-end. +- [ ] CI matrix for the optional dep — deferred. CI runs + `uv sync --all-extras`, so layer-2 and layer-3 tests already run on + every PR. Reconsider when we add an extra whose tests must NOT run + on the default lane. -We are integrating Pipelex with Mistral Workflows -(`mistralai-workflows>=3.3.0`), the Mistral orchestration framework that wraps -Temporal with a thicker DX layer. Two goals were considered: +--- -- **Goal 1** — Port Pipelex orchestration to run on Mistral Workflows as an - alternative durable runtime (replacing/duplicating our existing Temporal - integration). Outcome: feasible but high friction (3–4 weeks). **Deferred** - pending answers from the Mistral team about extension hooks for payload - converter, codec, sandbox, and run_worker. Out of scope for this plan. -- **Goal 2** — Let users invoke Pipelex pipes from inside their own Mistral - Workflows activities. Low risk, clear user value, ~2 weeks. **In scope.** +## 1. Context -This plan covers Goal 2 only. The `mistralai-workflows` dependency must remain -strictly optional. +Integrate Pipelex with Mistral Workflows (`mistralai-workflows>=3.3.0`), +which wraps Temporal with a thicker DX layer. Two goals were considered: ---- +- **Goal 1** — Port Pipelex orchestration to run *on* Mistral Workflows as + an alternative durable runtime. Blocked on Mistral exposing extension + hooks for payload converter, codec, sandbox passthrough, and a + bare-Temporal `run_worker` mode. **Out of scope.** File issues with the + Mistral team if/when we want to revisit. +- **Goal 2** — Let users invoke Pipelex pipes from inside their own + Mistral Workflows activities. **In scope.** Phase 1.x complete; Phase 2 + pending. -## 2. Background — what we know about Mistral Workflows - -Verified by reading the installed package -(`/Users/lchoquel/repos/Pipelex/_mistral/.venv/lib/python3.13/site-packages/mistralai/workflows/`): - -- Mistral Workflows IS Temporal underneath. Their `@workflow.define` decorator - ultimately calls `temporalio.workflow.defn(sandboxed=...)` (see - `mistralai/workflows/core/workflow.py:170, 251`). -- Their decorator wraps the user's `run` method so the workflow's runtime - signature becomes `run(self, params: dict | None)`. Caller side dumps params - via `params.model_dump()` (`core/execution/workflow_execution.py:192-204`). - This breaks any kajson-based subclass preservation at the workflow boundary - but **does not affect activities**, whose payload converter handles arg - serialization directly. -- The Mistral worker hardcodes `MistralWorkflowsPayloadConverter` and - `MistralWorkflowsPayloadCodec` (`core/worker.py:421-425`). No public override. -- `run_worker(workflows)` connects to the Mistral cloud control plane, - registers schemas, heartbeats. Requires `MISTRAL_API_KEY`. There is a - `mistralai.workflows.testing.create_test_worker` for in-process Temporal - test envs that does NOT require the cloud. -- Activities (`@workflows.activity`) are unrestricted Python — no sandbox, - no schema constraints beyond JSON-serializable types, no cloud dep at - invocation time. -- There is a "local execution" mode where `execute_workflow` runs the entry - method directly without Temporal — only useful for prototyping. -- `OffloadableField` (from `mistralai.extra.workflows`) + an - `ActivityInOutOffloadingInterceptor` provide automatic large-payload - offloading at the activity boundary. - -Pipelex side (verified by reading the repo): - -- `PipeJob` (`pipelex/pipe_run/pipe_job.py:13`) is a BaseModel that already - has `prepare_for_temporal()` — dehydrates `WorkingMemory` to a raw dict - when a `LibraryCrate` is present. -- Direct-mode execution: `PipeRun(pipe_router=...)` in - `pipelex/pipe_run/pipe_run.py:21`. May need a `make_direct_pipe_run()` - factory if absent — confirm in pre-flight. -- Temporal-mode execution: `make_temporal_pipe_run(...)` in - `pipelex/temporal/tprl_pipe/temporal_pipe_run.py:104`. Provides `.run()` - (blocking) and `.start()` (returns `(workflow_id, handle)`). -- `LibraryCrate` already round-trips through Pipelex's own Temporal codec — - same `model_dump`/`model_validate` will work for our boundary. -- Existing precedent for plugin layout: `pipelex/plugins/mistral/` (the - Mistral inference plugin). We follow the same shape. -- `pyproject.toml` already declares - `mistralai-workflows = ["mistralai-workflows>=3.3.0"]` in - `[project.optional-dependencies]`. No change needed there for Phase 1. +The `mistralai-workflows` dependency must remain strictly optional. --- -## 3. Locked-in design decisions - -- [x] **Library crate transport**: dump-based. - `PipelexPipeRunInput.library_crate_dump: dict[str, Any] | None`. No - registry-based variant in Phase 1; can be layered in later if asked. -- [x] **Execution mode**: an exhaustive `StrEnum`, set per-call (not - per-worker). Three modes: `DIRECT`, `TEMPORAL_BLOCKING`, - `TEMPORAL_FIRE_AND_FORGET`. -- [x] **Streaming**: in-scope, phased — Phase 2.0 ships a single Mistral - `task()` per activity (started/completed/failed); Phase 2.1 ships - per-step `task.update()` events driven by Pipelex's `report_delegate`, - conditional on demand. -- [x] **Plugin location**: `pipelex/plugins/mistralai_workflows/`. -- [x] **Optional-dep guard**: guard lives in `activities.py` (and will live in - `streaming.py` when added). `__init__.py` is empty per Pipelex's - "no re-exports" rule. `bridge.py`, `execution_mode.py`, `bootstrap.py`, - `exceptions.py` are framework-agnostic and importable on a venv that - does NOT have `mistralai-workflows` installed. -- [x] **Boundary types are JSON-only**. `inputs` and `output_dict` are - `dict[str, Any]`; `library_crate_dump` is a dict. No Pipelex internal - types (PipeJob, PipeOutput, WorkingMemory) cross the activity boundary. +## 2. Locked-in design decisions (still binding for Phase 2) + +- **Library crate transport** is dump-based: + `PipelexPipeRunInput.library_crate_dump: dict[str, Any] | None`. +- **Execution mode** is an exhaustive `StrEnum`, set per-call (not + per-worker): `DIRECT`, `TEMPORAL_BLOCKING`, `TEMPORAL_FIRE_AND_FORGET`. +- **Streaming** is phased. Phase 2.0 ships a single Mistral `task()` per + activity (started / completed / failed). Phase 2.1 ships per-step + `task.update()` events driven by Pipelex's `report_delegate`, + conditional on demand. +- **Plugin location**: `pipelex/plugins/mistralai_workflows/`. +- **Optional-dep guard** lives in `activities.py` (and will live in + `streaming.py`). `__init__.py` is empty per Pipelex's "no re-exports" + rule. `bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py` + are framework-agnostic and importable without `mistralai-workflows`. +- **Boundary types are JSON-only.** No Pipelex internal types (`PipeJob`, + `PipeOutput`, `WorkingMemory`) cross the activity boundary. --- -## 4. Module layout (final) +## 3. Module layout ``` pipelex/plugins/mistralai_workflows/ -├── __init__.py # optional-dep guard ONLY (no re-exports) -├── exceptions.py # 4 exception classes -├── execution_mode.py # PipelexExecutionMode StrEnum -├── bridge.py # framework-agnostic core (NO mistralai import) -├── bootstrap.py # ensure_pipelex_booted() + DI helper -├── activities.py # @activity-decorated wrappers -└── streaming.py # Phase 2: mistral task() event forwarding +├── __init__.py # empty (no re-exports) +├── exceptions.py # 4 exception classes +├── execution_mode.py # PipelexExecutionMode StrEnum +├── bridge.py # framework-agnostic core (NO mistralai imports) +├── bootstrap.py # ensure_pipelex_booted() + DI helper +├── activities.py # Tier-1 wrappers + offloaded variant +└── streaming.py # Phase 2 — NOT YET CREATED + +tests/unit/pipelex/plugins/mistralai_workflows/ +├── test_input_models.py # boundary BaseModels +├── test_execution_mode.py # StrEnum properties +├── test_validation.py # _validate_input + decode helpers +└── test_dispatch.py # mode dispatch with mocked PipeRun tests/integration/pipelex/plugins/mistralai_workflows/ -├── conftest.py # mistralai = pytest.importorskip(...) -├── test_bridge.py # layer 1: no optional dep needed -├── test_activities_direct.py # layer 2: needs mistralai-workflows -├── test_activities_temporal_blocking.py # layer 3: + temporal extra -└── test_activities_streaming.py # Phase 2 +├── conftest.py # bridge_test_library fixture +├── test_data/ # bridge_test.mthds + bridge_funcs.py +├── test_bridge_direct.py # layer 1 (no optional dep) +├── test_activities_direct.py # layer 2 (Mistral test worker) +├── test_activities_offloaded.py # layer 2 — offloaded variant +├── test_bridge_temporal_blocking.py # layer 3 (+ temporal extra) +└── test_bridge_temporal_fire_and_forget.py # layer 3 (+ temporal extra) ``` -Per Pipelex's "no re-exports in `__init__.py`" rule, users import from full -paths: +Users import from full paths (no re-exports per Pipelex rule): ```python -from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe +from pipelex.plugins.mistralai_workflows.activities import ( + pipelex_run_pipe, + pipelex_run_pipe_offloaded, + PipelexPipeRunInputOffloaded, + PipelexPipeRunOutputOffloaded, +) from pipelex.plugins.mistralai_workflows.bridge import ( PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge, ) from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -``` - -The `__init__.py` exists only to host the import-time guard. - ---- - -## 5. Public API — three usage tiers - -### Tier 1 — pre-decorated activity - -```python -from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - -@workflows.workflow.define(name="my-flow") -class MyFlow: - @workflows.workflow.entrypoint - async def run(self, doc_url: str) -> dict: - result = await pipelex_run_pipe(PipelexPipeRunInput( - pipe_code="extract_invoice", - inputs={"doc_url": doc_url}, - execution_mode=PipelexExecutionMode.DIRECT, - )) - return result.output_dict -``` - -### Tier 2 — bridge helper inside user's own typed activity - -```python -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, run_pipe_via_bridge, -) - -@workflows.activity(start_to_close_timeout=timedelta(minutes=30), rate_limit=quota) -async def extract_invoice(doc_url: str) -> InvoiceData: - out = await run_pipe_via_bridge(PipelexPipeRunInput( - pipe_code="extract_invoice", inputs={"doc_url": doc_url}, - )) - return InvoiceData.model_validate(out.output_dict) -``` - -`run_pipe_via_bridge` is the same code the Tier-1 activity calls — just -without the `@activity` decoration. Lets users own per-pipe activity -configuration (timeouts, rate limits, sticky-to-worker, names). - -### Tier 3 — full control - -Use `build_pipe_job_from_input(...)` and `serialize_pipe_output(...)` from -`bridge.py` directly. For multi-pipe activities, custom delivery, or test -fixtures. - ---- - -## 6. Concrete designs — file by file - -### `execution_mode.py` - -```python -from pipelex.types import StrEnum - -class PipelexExecutionMode(StrEnum): - """How a Pipelex pipe runs inside a Mistral Workflows activity. - - DIRECT: in-process; no Temporal involved on Pipelex's side; activity - blocks until the pipe completes. Fastest feedback, simplest ops. - TEMPORAL_BLOCKING: dispatch the pipe as a Pipelex Temporal workflow; - the activity awaits completion. Pipe runs durably on Pipelex's - worker fleet. Requires pipelex[temporal] extra. - TEMPORAL_FIRE_AND_FORGET: dispatch the pipe as a Pipelex Temporal - workflow and return immediately with the workflow_id. Activity - does NOT wait; completion is signalled out-of-band via - DeliveryAssignment (webhook / storage). Same dep requirements - as TEMPORAL_BLOCKING. delivery_assignment_dump is required. - """ - - DIRECT = "direct" - TEMPORAL_BLOCKING = "temporal_blocking" - TEMPORAL_FIRE_AND_FORGET = "temporal_fire_and_forget" - - @property - def requires_pipelex_temporal(self) -> bool: - match self: - case PipelexExecutionMode.DIRECT: - return False - case ( - PipelexExecutionMode.TEMPORAL_BLOCKING - | PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET - ): - return True -``` - -Exhaustive `match` (no `case _:`) — Pipelex linting requires this and we get -linter errors when a new mode is added. - -### `exceptions.py` - -```python -from pipelex.exceptions import PipelexError - -class MistralWorkflowsPluginError(PipelexError): - pass - -class MistralWorkflowsNotInstalledError(MistralWorkflowsPluginError, ImportError): - pass - -class MissingPipelexTemporalExtraError(MistralWorkflowsPluginError): - pass - -class PipelexBridgeRuntimeError(MistralWorkflowsPluginError): - pass -``` - -### `bootstrap.py` - -```python -from pathlib import Path -from typing import Callable -from pipelex import Pipelex -from pipelex.system.runtime import RunMode - -_BOOTED = False - -def ensure_pipelex_booted( - config_dir: Path | None = None, - force_run_mode: RunMode | None = None, -) -> None: - """Idempotent. Boots Pipelex on first call; no-op afterwards.""" - global _BOOTED - if _BOOTED: - return - Pipelex.make(config_dir=config_dir, run_mode=force_run_mode) - _BOOTED = True - -def get_pipelex_dependency() -> Callable[[], Pipelex]: - """Returns a callable suitable for mistralai.workflows Depends(...).""" - def _resolver() -> Pipelex: - ensure_pipelex_booted() - return Pipelex.get_instance() - return _resolver -``` - -User's worker entry-point: - -```python -async def main() -> None: - ensure_pipelex_booted() # boot once before workers start - await workflows.run_worker([MyFlow], activities=[pipelex_run_pipe]) -``` - -We don't auto-magic the boot from inside the activity for production -correctness — users should know Pipelex is running. The defensive call -inside `run_pipe_via_bridge` exists only to make first-time-runner mistakes -survivable. - -### `bridge.py` - -NO `mistralai.workflows` or `temporalio` imports at module top-level. -The Temporal extra is lazy-imported inside the temporal-mode branches. - -```python -from typing import Any -from pydantic import BaseModel, ConfigDict, Field -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted -from pipelex.plugins.mistralai_workflows.exceptions import ( - PipelexBridgeRuntimeError, MissingPipelexTemporalExtraError, -) - - -class PipelexPipeRunInput(BaseModel): - """JSON-safe input crossing the Mistral/Temporal boundary.""" - model_config = ConfigDict(extra="forbid") - - pipe_code: str - domain_code: str | None = None - inputs: dict[str, Any] = Field(default_factory=dict) - output_name: str | None = None - pipeline_run_id: str | None = None # generated if None - user_id: str | None = None - library_crate_dump: dict[str, Any] | None = None - execution_mode: PipelexExecutionMode = PipelexExecutionMode.DIRECT - delivery_assignment_dump: dict[str, Any] | None = None - - -class PipelexPipeRunOutput(BaseModel): - """JSON-safe output crossing the Mistral/Temporal boundary.""" - model_config = ConfigDict(extra="forbid") - - output_dict: dict[str, Any] - main_stuff_name: str | None = None - pipeline_run_id: str - workflow_id: str | None = None # set when execution_mode is TEMPORAL_* - is_completed: bool # False for FIRE_AND_FORGET - graph_spec_dump: dict[str, Any] | None = None - - -def build_pipe_job_from_input(input: PipelexPipeRunInput) -> "PipeJob": - """Hydrate a PipeJob from JSON-safe input. Loads library_crate if given.""" - ... - -def serialize_pipe_output(output: "PipeOutput") -> dict[str, Any]: - """Dehydrate PipeOutput to JSON-safe dict via dump_for_json/temporal.""" - ... - - -async def run_pipe_via_bridge(input: PipelexPipeRunInput) -> PipelexPipeRunOutput: - ensure_pipelex_booted() - _validate_input(input) - pipe_job = build_pipe_job_from_input(input) - delivery = _build_delivery_assignment(input.delivery_assignment_dump) - - match input.execution_mode: - case PipelexExecutionMode.DIRECT: - return await _run_direct(pipe_job, delivery) - case PipelexExecutionMode.TEMPORAL_BLOCKING: - _require_pipelex_temporal_extra() - return await _run_temporal_blocking(pipe_job, delivery) - case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: - _require_pipelex_temporal_extra() - return await _run_temporal_fire_and_forget(pipe_job, delivery) - - -def _validate_input(input: PipelexPipeRunInput) -> None: - if ( - input.execution_mode is PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET - and input.delivery_assignment_dump is None - ): - msg = ( - "TEMPORAL_FIRE_AND_FORGET requires a delivery_assignment_dump; " - "otherwise the pipe completion is silently dropped." - ) - raise PipelexBridgeRuntimeError(msg) - - -def _require_pipelex_temporal_extra() -> None: - try: - import temporalio # noqa: F401, PLC0415 - except ImportError as exc: - msg = ( - "TEMPORAL_* execution modes require the pipelex[temporal] extra. " - "Install with: pip install 'pipelex[temporal,mistralai-workflows]'" - ) - raise MissingPipelexTemporalExtraError(msg) from exc ``` -`_run_direct`, `_run_temporal_blocking`, `_run_temporal_fire_and_forget` are -private helpers; they wrap pipe-run failures into -`PipelexBridgeRuntimeError` (chained from the original exception). No -`except Exception` per Pipelex standards — only catch -`PipeRunError`/`PipeJobError` (and `WorkflowExecutionError` for temporal -modes) explicitly. - -### `__init__.py` - -```python -from pipelex.plugins.mistralai_workflows.exceptions import ( - MistralWorkflowsNotInstalledError, -) - -try: - import mistralai.workflows # noqa: F401 -except ImportError as exc: - msg = ( - "The 'mistralai-workflows' optional dependency is not installed. " - "Install with: pip install 'pipelex[mistralai-workflows]'" - ) - raise MistralWorkflowsNotInstalledError(msg) from exc -``` - -Note: `bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py` can -be imported even when `mistralai-workflows` is NOT installed, because the -guard is in `__init__.py` — but only triggers when the package itself is -imported. To preserve this, **users importing the framework-agnostic -modules must import them via `pipelex.plugins.mistralai_workflows.bridge` -etc., which will run the guard first.** This means the optional dep IS -required even for Tier-3 use. If we want the framework-agnostic core to be -usable without the optional dep, move the guard out of `__init__.py` and -into `activities.py` + `streaming.py` only. - -**Decision pending in pre-flight**: should `bridge.py` / `execution_mode.py` -be importable without `mistralai-workflows`? Recommended **yes**, so move -the guard into `activities.py` and `streaming.py` only. (Updated -recommendation overrides §4 / §8 of original design draft.) - -### `activities.py` - -```python -from datetime import timedelta -from mistralai.workflows import activity # this triggers ImportError if missing - -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge, -) - - -@activity( - start_to_close_timeout=timedelta(minutes=10), - retry_policy_max_attempts=3, -) -async def pipelex_run_pipe(input: PipelexPipeRunInput) -> PipelexPipeRunOutput: - return await run_pipe_via_bridge(input) -``` - -Future: an `OffloadableField`-using variant for large payloads (Phase 1.5). - --- -## 7. Pre-flight verification (before Phase 1.0) - -- [x] `PipeOutput` exposes `prepare_for_temporal(library_crate)` that delegates - to `WorkingMemory.dump_for_temporal()`. We don't need a new - `dump_for_json()`. The bridge serializes via - `working_memory.dump_for_temporal()` directly so the output shape is - consistent regardless of `library_crate`. -- [x] No `make_direct_pipe_run()` factory needed. `PipeRouter()` has no - required args; the bridge constructs `PipeRun(pipe_router=PipeRouter())` - inline inside `_run_direct`. -- [x] `LibraryCrate.model_dump()` / `model_validate()` round-trip cleanly - (verified by `test_bridge_direct.test_direct_mode_with_library_crate_dump`). -- [x] `Pipelex.make()` is **NOT** idempotent — it raises if a singleton - already exists. `bootstrap.ensure_pipelex_booted()` calls - `Pipelex.make()` only when `Pipelex.get_optional_instance() is None`, - which adopts an externally-booted singleton without re-initializing. -- [x] **Guard placement**: chose to put the optional-dep guard in - `activities.py` only (and in `streaming.py` when added). `__init__.py` - is empty. `bridge.py`, `execution_mode.py`, `bootstrap.py`, and - `exceptions.py` are importable without `mistralai-workflows`. -- [x] `DeliveryAssignment.model_dump()` / `model_validate()` round-trip - cleanly (plain BaseModel; verified in `test_validation`). +## 4. Gotchas from Phase 1 (read before Phase 2) ---- +These bit during Phase 1.1–1.2 and will likely bite again when adding +`streaming.py` plus a new integration test module. Save the cycles. -## 8. Phasing & checklist - -### Phase 1.0 — Framework-agnostic core (no optional dep) — **DONE** - -Files: `bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`. -None of these import `mistralai.workflows` at module top-level. `bridge.py` -lazy-imports `temporalio` only inside the temporal-mode branches. - -- [x] Create `pipelex/plugins/mistralai_workflows/` package directory -- [x] Create `__init__.py` (empty — guard lives in `activities.py` only) -- [x] Create `exceptions.py` with all 4 exception classes - (`MistralWorkflowsPluginError`, `MistralWorkflowsNotInstalledError`, - `MissingPipelexTemporalExtraError`, `PipelexBridgeRuntimeError`) -- [x] Create `execution_mode.py` with `PipelexExecutionMode` StrEnum and - `requires_pipelex_temporal` + `is_fire_and_forget` properties using - exhaustive `match/case` -- [x] Create `bootstrap.py` with `ensure_pipelex_booted()` (idempotent via - `Pipelex.get_optional_instance()` singleton check — no module-level - flag needed) and `get_pipelex_dependency()` factory -- [x] Create `bridge.py`: - - [x] `PipelexPipeRunInput` BaseModel (`extra="forbid"`) - - [x] `PipelexPipeRunOutput` BaseModel (`extra="forbid"`) - - [x] `build_pipe_job_from_input(input) -> PipeJob` - - [x] `serialize_pipe_output(pipe_output) -> dict[str, Any]` — - always uses `WorkingMemory.dump_for_temporal()` for stable shape - - [x] `run_pipe_via_bridge(input) -> PipelexPipeRunOutput` with - exhaustive mode dispatch - - [x] `_run_direct`, `_run_temporal_blocking`, - `_run_temporal_fire_and_forget` private helpers - - [x] `_require_pipelex_temporal_extra()` lazy-import guard - - [x] `_validate_input()`: FIRE_AND_FORGET + no delivery → raise - - [x] `_scoped_library_for_crate()` async context manager for per-call - scoped library when a `library_crate_dump` is provided -- [x] Layer-1 tests — **split across modules** (1 TestClass per module per - Pipelex pytest standards): - - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py` - — input/output BaseModel validation (forbid extra, required - fields, defaults, JSON round-trip) - - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py` - — `PipelexExecutionMode` properties - - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py` - — `_validate_input` (FIRE_AND_FORGET requires delivery), - `_decode_library_crate` / `_decode_delivery_assignment` - round-trips, `run_pipe_via_bridge` validation error path - - [x] `tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py` - — DIRECT / TEMPORAL_BLOCKING / TEMPORAL_FIRE_AND_FORGET dispatch - with mocked `PipeRun.run` and `make_temporal_pipe_run` (uses - `PipeJob.model_construct` to bypass Pydantic's pipe validation) - - [x] `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` - — DIRECT-mode end-to-end against a real loaded `PipeFunc` test - pipe; covers globally-loaded library, `library_crate_dump` - round-trip, and caller-supplied `pipeline_run_id` -- [x] `make agent-check` passes (lint, ruff, pyright, mypy) -- [ ] `make agent-check` passes with `mistralai-workflows` NOT installed - (not yet verified — needs a fresh venv without the extra) - -### Phase 1.1 — Tier 1 activity wrapper — **DONE** (modulo optional pytest marker) - -- [x] Create `activities.py` (top-of-file imports `mistralai.workflows` — - raises `MistralWorkflowsNotInstalledError` with install hint if missing) -- [x] `pipelex_run_pipe` — `@activity(start_to_close_timeout=10min, - retry_policy_max_attempts=3)`-decorated wrapper around - `run_pipe_via_bridge` -- [x] Create `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` - with the `bridge_test_library` fixture (loads test pipe + registers - its `mistralai_workflows_bridge_echo` PipeFunc target). The - `pytest.importorskip("mistralai.workflows")` lives at module-level of - `test_activities_direct.py` so the rest of the dir (layer-1 bridge - tests) is NOT skipped when the optional dep is missing. -- [x] Layer-2 integration test (`test_activities_direct.py`): - - [x] Spin `WorkflowEnvironment.start_local` + `create_test_worker` - - [x] Define a test workflow that calls `pipelex_run_pipe` in DIRECT - mode against the registered PipeFunc test pipe - - [x] Assert `output_dict` shape and `is_completed=True` -- [ ] (Optional) Add `mistralai_workflows` pytest marker to - `pyproject.toml` markers — **deferred**: the module-level - `importorskip` already gates the test correctly without a marker. -- [ ] (Optional) Extend `[tool.pytest] addopts` default `-m` filter to - exclude `mistralai_workflows` — **deferred** for the same reason. - -**Issues uncovered + fixed in Phase 1.1**: - -1. *Workflow sandbox* rejected pipelex imports during workflow class - validation. Fixed by wrapping pipelex imports in +1. **Workflow sandbox rejects Pipelex imports** seen during workflow + class validation. Wrap them in `mistralai_workflows.workflow.unsafe.imports_passed_through()` AND - passing `enforce_determinism=False` to `@workflow.define` for the test - workflow. -2. *Search attribute* — Mistral's `@workflow.define` wrapper upserts an - `OtelTraceId` keyword search attribute on every workflow run. The dev - server rejects the activation if the attribute isn't pre-registered, - so the test passes `search_attributes=[SearchAttributeKey.for_keyword( - "OtelTraceId")]` to `WorkflowEnvironment.start_local`. -3. *Task queue mismatch* — Mistral's `@activity` wrapper dispatches via - `temporalio.workflow.execute_activity(..., task_queue= - config.get_effective_task_queue())`, which reads the **global** - `mistralai_config.temporal.task_queue` (default `"default"`) — NOT the - workflow's task queue. If we don't override it, activities are - scheduled on `"default"` while the test worker polls our test queue, - causing the workflow to hang. Fixed by an autouse module-scoped - fixture in `test_activities_direct.py` that pins + pass `enforce_determinism=False` to `@workflow.define` for test + workflows. See `test_activities_direct.py:24-30` for the pattern. + Activities are unrestricted Python — this only matters inside + workflow class bodies. + +2. **`OtelTraceId` search attribute** must be pre-registered on the + test namespace. Mistral's `@workflow.define` upserts it on every + workflow run, and the dev server rejects activations against an + unknown attribute. Pass + `search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")]` + to `WorkflowEnvironment.start_local`. + +3. **Task queue mismatch.** Mistral's `@activity` wrapper schedules + activities on the *global* `mistralai_config.temporal.task_queue` + (default `"default"`), NOT the workflow's task queue. Spin a worker + on a custom test queue without overriding the global → activities + land on `"default"` and the workflow hangs forever. Fix: an autouse + module-scoped fixture pinning `mistralai_config.temporal.task_queue = TEST_TASK_QUEUE` for the - duration of the module and restores it on teardown. -4. *Result shape* — Mistral's `convert_result_to_temporal_format` wraps - non-BaseModel returns in `{"result": ...}`. Returning - `PipelexPipeRunOutput` (a BaseModel) directly from the workflow's - entrypoint avoids the wrapping — no shape mangling. -5. *mypy* — `mistralai-workflows`'s own source uses PEP 695 type-parameter - syntax that mypy rejects under `python_version=3.11`. Added a - `[[tool.mypy.overrides]]` block in `pyproject.toml` with - `follow_imports = "skip"` and `ignore_errors = true` for - `mistralai.workflows.*`. - -### Phase 1.2 — Temporal modes — **DONE** - -Wiring was already in place in `bridge.py` from Phase 1.0; this phase added -the layer-3 integration tests against a real Pipelex Temporal worker. - -- [x] `_run_temporal_blocking` wired to `make_temporal_pipe_run().run(...)` -- [x] `_run_temporal_fire_and_forget` wired to - `make_temporal_pipe_run().start(...)` — returns immediately with - `workflow_id` and `is_completed=False` -- [x] Layer-3 integration test - (`test_bridge_temporal_blocking.py`): - - [x] `pytest.importorskip("temporalio")` at module level - - [x] Boots Pipelex's Temporal layer (`TemporalTaskManager` + - `make_temporal_pipe_router` + `ContentGeneratorChild`) and - pre-connects `TemporalManager` to the test env's client - - [x] Calls `run_pipe_via_bridge` (Tier-3 entry point) in - TEMPORAL_BLOCKING mode — same code path the Mistral activity - wrapper uses, just without the single-line `@activity` - decoration (which Layer 2 already validates for DIRECT mode). - - [x] Asserts `is_completed=True`, `workflow_id` is set, and the - pipe's output round-trips through `WfPipeRouter` -- [x] Layer-3 fire-and-forget test - (`test_bridge_temporal_fire_and_forget.py`): - - [x] Bridge returns immediately with non-None `workflow_id` and - `is_completed=False`; `output_dict={}`, `main_stuff_name=None` - - [x] Verifies the Pipelex workflow eventually completes - (`WorkflowExecutionStatus.COMPLETED` after `handle.result()`) - - [x] Empty `DeliveryAssignment()` (no storage, no webhooks) — - satisfies `_validate_input` without depending on external - webhook infrastructure - -**Notes & follow-ups**: - -- `bridge_test.mthds` got a second pipe `bridge_compose_pipe` (PipeCompose - with a Jinja2 template). This pipe is Temporal-compatible, whereas - `bridge_func_pipe` (PipeFunc) is not — `asyncio.to_thread` inside - `PipeFunc` raises `NotImplementedError` in the deterministic workflow - event loop. Tests pick the appropriate pipe per mode. -- A wider layer-3 test that wraps the bridge in the Mistral activity - (`@activity`-decorated `pipelex_run_pipe`) and dispatches both the - Mistral workflow and the Pipelex Temporal workflow on the same in-process - Temporal server is feasible but heavier (parallel Mistral test worker + - Pipelex worker + cross-converter compatibility checks). The Mistral - activity wrapping is a single-line `@activity` decoration over - `run_pipe_via_bridge` already validated end-to-end for DIRECT mode in - `test_activities_direct.py`, so no new branching logic is exercised by - re-running the same wrapping for TEMPORAL_*. Deferred unless we add - TEMPORAL_*-specific behavior to the activity layer. - -### Phase 1.3 — Docs, changelog, CI matrix - -- [x] Write `docs/under-the-hood/mistralai-workflows-plugin.md` (overview + - install + when to use which `PipelexExecutionMode`) and wire it into - `mkdocs.yml` -- [x] Write `docs/under-the-hood/mistralai-workflows-recipes.md` with - worked examples: Tier 1, Tier 2, library_crate (including - `TEMPORAL_FIRE_AND_FORGET` with a `DeliveryAssignment` example) -- [x] Update `CHANGELOG.md` Unreleased: "Added: Pipelex pipes can now be - invoked from inside Mistral Workflows activities via the new - `pipelex.plugins.mistralai_workflows` plugin." -- [ ] CI matrix — **deferred**: the existing CI (`tests-check.yml`) runs - `make install` which calls `uv sync --all-extras`, so layer 2 and 3 - tests already run on every PR. Pipelex doesn't currently maintain - separate per-extras CI lanes for any other optional dep; introducing - the convention solely for `mistralai-workflows` is out of scope. - Reconsider when we add an extra whose tests must NOT run on the - default lane. -- [ ] Cookbook example — **deferred to a follow-up PR in - `pipelex-cookbook/`**. That repo is a sibling, not part of this - worktree. Suggested entry: `examples/c_advanced/mistral-workflows/` - with a Tier-1 DIRECT-mode worker + a Tier-2 typed activity using - `library_crate_dump`. - -### Phase 1.5 — Large payload offloading - -- [ ] Add an `OffloadableField`-using variant of `PipelexPipeRunInput` / - `PipelexPipeRunOutput` in `activities.py` (the import lives behind - the optional-dep boundary, fine) -- [ ] Wire the variant into a second pre-decorated activity - `pipelex_run_pipe_offloaded`, OR add a parameter to the existing one -- [ ] Test with a large fixture (>2MB) to confirm offload path works - end-to-end with Mistral's `ActivityInOutOffloadingInterceptor` -- [ ] Document the trade-off (output stored in Mistral-managed storage) - -### Phase 2.0 — Streaming v1 (one task per activity) - -- [ ] Create `streaming.py` (imports `mistralai.workflows`) -- [ ] Wrap `pipelex_run_pipe` body in `async with workflows.task(...) as t:` -- [ ] Emit `started` event with `pipe_code` + `pipeline_run_id` -- [ ] Emit `completed` with output summary on success -- [ ] Emit `failed` with exception details on error -- [ ] Layer-4 streaming test using `create_test_worker_with_events` + - `create_capturing_mock_events_client` - -### Phase 2.1 — Streaming v2 (per-step granularity, conditional on demand) - -- [ ] Subscribe to `report_delegate` event stream from inside the activity -- [ ] Map Pipelex events to Mistral `task.update(...)` calls: - - Pipe sub-step started → `in_progress` with description - - Stuff added to working memory → `in_progress` with new key - - Pipe step completed → progress % -- [ ] Forwarder side-task: drain event log; terminate cleanly when the - activity returns; cover both success and failure paths -- [ ] Test: assert per-step events emitted in correct order for a - multi-step pipe + duration of the test module. See `test_activities_direct.py:48-64`. + +4. **Result shape.** Mistral's `convert_result_to_temporal_format` + wraps non-BaseModel return values in `{"result": ...}`. Return a + BaseModel directly from the workflow entrypoint to skip the + wrapping (we return `PipelexPipeRunOutput`). + +5. **mypy + Mistral's PEP 695 type syntax.** `mistralai-workflows`'s + own source uses PEP 695 type parameters that mypy rejects under + `python_version=3.11`. The `[[tool.mypy.overrides]]` block for + `mistralai.workflows.*` (`follow_imports = "skip"`, + `ignore_errors = true`) in `pyproject.toml` is already in place. + Mypy will still flag `OffloadableField.get_value()` as returning + `Any` — assign through a typed intermediate variable. + +6. **`PipeFunc` is NOT Temporal-compatible.** `asyncio.to_thread` + inside `PipeFunc` raises `NotImplementedError` in the deterministic + workflow event loop. The bundle ships `bridge_func_pipe` (PipeFunc, + DIRECT only) for the no-Temporal layers and `bridge_compose_pipe` + (PipeCompose, Temporal-compatible) for the Temporal layers; the + `bridge_envelope_pipe` (PipeCompose with inline-structured concept) + exists to exercise dynamic-concept round-trip via + `library_crate_dump`. Pick the right one per execution mode. + +7. **`Pipelex.make()` is NOT idempotent** — it raises if a singleton + already exists. `bootstrap.ensure_pipelex_booted()` only calls it + when `Pipelex.get_optional_instance() is None`, so an externally- + booted singleton is adopted. Don't replace this guard with a + module-level boolean. --- -## 9. Pyproject.toml changes — actual - -**Applied**: - -- Added a `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` with - `follow_imports = "skip"` and `ignore_errors = true` (mistralai's source - uses PEP 695 type syntax that mypy rejects under `python_version=3.11`). - -**Deferred** — the module-level `pytest.importorskip("mistralai.workflows")` -in `test_activities_direct.py` already gates the layer-2 test correctly -without a marker. Reconsider if test runtime grows or other tests need to -opt in/out of the optional dep: - -```toml -# tool.pytest markers — not yet added -"mistralai_workflows: tests that require the mistralai-workflows optional dependency", -``` - -```toml -# tool.pytest addopts default exclusion — not yet extended -"-m", "not (inference or llm or img_gen or extract or search or pipelex_api or mistralai_workflows)", -``` - -`[project.optional-dependencies].mistralai-workflows` already declared: -`["mistralai-workflows>=3.3.0"]`. No change. +## 5. Phase 2 — Streaming + +Goal: surface live progress events from Pipelex pipes through Mistral's +`task()` event API so users can subscribe via +`create_capturing_mock_events_client` and friends. + +### Phase 2.0 — One task per activity (started / completed / failed) + +A single Mistral task wraps the whole activity body — no per-step +granularity. Cheapest path to "the user sees something happen." + +- [ ] Create `pipelex/plugins/mistralai_workflows/streaming.py`. Top-of- + file imports `mistralai.workflows` — reuse the same optional-dep + guard pattern as `activities.py`. +- [ ] **Decision (open):** wrap `pipelex_run_pipe` directly, OR ship a + sibling activity `pipelex_run_pipe_streaming`? + - Wrapping the existing one is one fewer activity to register but + forces every caller to pay the Mistral `task()` overhead. + - A sibling activity keeps the silent path silent. Leaning toward + **sibling**; revisit when we measure the overhead. +- [ ] Inside the wrapper: `async with workflows.task(...) as t:`, then: + - emit `started` with `pipe_code` + `pipeline_run_id` + - emit `completed` with an output summary on success + - emit `failed` with exception details on error + - catch the same specific exceptions `bridge.py::_run_*` already + catches (`PipeRunError`, `PipeJobError`, `PipeRouterError`, + `PipeExecutionError`, `PipelineExecutionError`) — never catch + generic `Exception` per Pipelex standards. +- [ ] New layer-4 integration test + `tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py` + using `create_test_worker_with_events` + + `create_capturing_mock_events_client`. Re-apply gotchas §4.1–4.4. + +### Phase 2.1 — Per-step granularity (only if 2.0 is too coarse) + +- [ ] Subscribe to Pipelex's `report_delegate` event stream from inside + the activity. +- [ ] Map Pipelex events to Mistral `task.update(...)` calls: + - pipe sub-step started → `in_progress` with description + - stuff added to working memory → `in_progress` with new key + - pipe step completed → progress % +- [ ] Forwarder side-task drains the event log and terminates cleanly on + both success and failure. Use `try` / `finally` (NOT `try` / + `except Exception`) for cleanup, per Pipelex standards. +- [ ] Test asserts per-step events emitted in correct order for a + multi-step pipe. The current `bridge_test.mthds` has only single- + step pipes — Phase 2.1 needs a multi-step fixture (e.g. a + `PipeSequence` chaining two `PipeCompose`s). --- -## 10. Phase 1 done criteria - -- [ ] Plugin module compiles and `make agent-check` passes with - `mistralai-workflows` installed -- [ ] `make agent-check` passes with `mistralai-workflows` NOT installed - (no spurious imports) -- [ ] Layer-1 tests pass on a no-extras venv -- [ ] Layer-2 tests pass on `[dev,mistralai-workflows]` (DIRECT mode e2e) -- [ ] Layer-3 tests pass on `[dev,mistralai-workflows,temporal]` - (TEMPORAL_BLOCKING + FIRE_AND_FORGET) -- [ ] `pipelex_run_pipe` round-trips a pipe with dynamic-concept output - via `library_crate_dump` -- [ ] Documentation published; CHANGELOG entry merged -- [ ] Cookbook example added +## 6. Open risks (track but don't block) ---- - -## 11. Risks / open items (track but don't block) - -- [ ] FIRE_AND_FORGET footgun mitigation — validation in - `run_pipe_via_bridge` before mode dispatch (covered in design) -- [ ] Pipelex bootstrap inside an already-bootstrapped Mistral worker — - confirm singleton guard is reentrant (pre-flight) -- [ ] Concurrent activities sharing process-global Pipelex state — reuse - per-call library scoping from - `pipelex/temporal/tprl_pipe/wf_pipe_router.py`; verify no leakage - under concurrent activity load -- [ ] Mistral's payload converter calls `params.model_dump()` at the - *workflow* call site only; activities use the converter directly. - Our boundary is JSON-only, so no kajson preservation needed — - confirm by integration test with a pipe that produces a - dynamic-concept output -- [ ] If Mistral upgrades break our use of `OffloadableField` location, - revisit (currently `mistralai.extra.workflows`) -- [ ] **Goal 1 deferred**: porting Pipelex orchestration to Mistral - Workflows as an alternative durable runtime is blocked on Mistral - adding extension hooks for payload converter, codec, sandbox - passthrough, and a bare-Temporal `run_worker` mode. File issues - with the Mistral team if/when we want to revisit. +- [ ] Concurrent activities sharing process-global Pipelex state — already + mitigated by per-call library scoping in + `bridge.py::_scoped_library_for_crate`. Verify no leakage under + concurrent load when we get there. +- [ ] If Mistral upgrades change the `OffloadableField` import path + (currently `mistralai.workflows.core.encoding.fields_offloader`), + revisit the imports in `activities.py`. --- -## 12. Resuming a session - -1. Read this file end-to-end. -2. Find the first unchecked box. If it's in §7 (pre-flight), resolve those - first — they may change the design (e.g. guard placement decision). -3. Implement the next phase's items in order, checking off boxes as you go. -4. Update §3 (locked-in decisions) only when an explicit user decision - changes the design; otherwise the design in §4–§6 is authoritative. -5. After each phase, run `make agent-check` and `make agent-test` (with - the appropriate extras installed for the phase) before moving on. +## 7. Resuming a session + +1. Read this file end-to-end — it's short by design. +2. **For anything in Phase 1.x: the code is authoritative.** Read + `pipelex/plugins/mistralai_workflows/*.py` and its tests; do not + re-derive from this doc. +3. For Phase 2 work: start at §5, pick the first unchecked box, re-read + §4 (gotchas) before scaffolding the new test module. +4. After each phase, run: + ``` + make agent-check + .venv/bin/pytest tests/unit/pipelex/plugins/mistralai_workflows/ \ + tests/integration/pipelex/plugins/mistralai_workflows/ + ``` + Before declaring Phase 2 done, run the broader sweep: + ``` + .venv/bin/pytest -n auto \ + -m "(dry_runnable or not (inference or llm or img_gen or extract or search)) and not pipelex_api" \ + tests/unit/pipelex/plugins/ tests/integration/pipelex/plugins/ \ + tests/unit/pipelex/builder/ tests/integration/pipelex/builder/ + ``` +5. Update the status board and the §5 boxes as you go. diff --git a/docs/under-the-hood/mistralai-workflows-recipes.md b/docs/under-the-hood/mistralai-workflows-recipes.md index 896d9ba08..cf6d808ea 100644 --- a/docs/under-the-hood/mistralai-workflows-recipes.md +++ b/docs/under-the-hood/mistralai-workflows-recipes.md @@ -51,6 +51,62 @@ The activity has sensible defaults (10 minute timeout, 3 retries). When you need --- +## Large payloads — `pipelex_run_pipe_offloaded` + +Temporal's per-event payload limit is around 2 MiB. When a pipe input or output approaches that ceiling — large documents, accumulated transcripts, image bytes — the activity rejects with `MessageTooLarge`. Mistral Workflows ships an `ActivityInOutOffloadingInterceptor` that streams oversized payloads through blob storage (S3/GCS/Azure) automatically, and Pipelex provides an offload-capable activity to plug into it. + +```python +from mistralai import workflows +from mistralai.workflows.core.encoding.fields_offloader import OffloadableField + +from pipelex.plugins.mistralai_workflows.activities import ( + PipelexPipeRunInputOffloaded, + PipelexPipeRunOutputOffloaded, + pipelex_run_pipe_offloaded, +) +from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput + + +@workflows.workflow.define(name="extract-large-doc-flow") +class ExtractLargeDocFlow: + @workflows.workflow.entrypoint + async def run(self, doc_bytes: bytes) -> dict: + wrapped_input = PipelexPipeRunInputOffloaded( + payload=OffloadableField( + value=PipelexPipeRunInput( + pipe_code="finance.extract_large_invoice", + inputs={"doc_bytes": doc_bytes.hex()}, + ), + ), + ) + wrapped_output: PipelexPipeRunOutputOffloaded = await pipelex_run_pipe_offloaded(wrapped_input) + return wrapped_output.payload.get_value().output_dict +``` + +The wrapping/unwrapping is a no-op when the payload fits inline. Offloading only kicks in when the worker is configured with the interceptor: + +```python +from mistralai import workflows +from mistralai.workflows.core.config.config import config +from mistralai.workflows.core.encoding.fields_offloader import FieldsOffloader +from mistralai.workflows.core.temporal.activity_offloading_interceptor import ( + ActivityInOutOffloadingInterceptor, +) + +offloader = FieldsOffloader(offloading_config=config.payload_offloading) +interceptor = ActivityInOutOffloadingInterceptor(offloader) + +await workflows.run_worker( + [ExtractLargeDocFlow], + activities=[pipelex_run_pipe_offloaded], + interceptors=[interceptor], +) +``` + +Trade-off: offloaded payloads live in the blob storage you configure (S3 by default in Mistral's example) for the lifetime of the workflow run. They incur storage cost and add an extra round-trip per offloaded field. Reach for the offloaded variant only when you actually need the size headroom. + +--- + ## Tier 2 — helper inside your own typed activity Wrap `run_pipe_via_bridge` in your own `@activity`-decorated function so you control all activity options and the input/output types. diff --git a/pipelex/plugins/mistralai_workflows/activities.py b/pipelex/plugins/mistralai_workflows/activities.py index 85537e470..76da0755f 100644 --- a/pipelex/plugins/mistralai_workflows/activities.py +++ b/pipelex/plugins/mistralai_workflows/activities.py @@ -3,6 +3,16 @@ Importing this module triggers the optional-dep guard: if ``mistralai-workflows`` is not installed, the import fails fast with a ``MistralWorkflowsNotInstalledError`` carrying install instructions. + +Two activity variants are exposed: + +- ``pipelex_run_pipe`` — inline boundary types. Use when payloads stay below + Temporal's per-event size limit (~2 MiB). +- ``pipelex_run_pipe_offloaded`` — boundary types wrapped in + ``OffloadableField`` so Mistral's ``ActivityInOutOffloadingInterceptor`` + can stream the payload through blob storage when it exceeds the configured + threshold. Requires the user to register the interceptor on their worker + (see Mistral's ``workflow_activity_offloading`` example). """ from datetime import timedelta @@ -16,6 +26,7 @@ try: from mistralai.workflows import activity + from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel except ImportError as exc: msg = ( "The 'mistralai-workflows' optional dependency is required to use " @@ -25,6 +36,26 @@ raise MistralWorkflowsNotInstalledError(msg) from exc +class PipelexPipeRunInputOffloaded(OffloadableModel): + """Offload-capable variant of ``PipelexPipeRunInput``. + + Wraps the inline ``PipelexPipeRunInput`` in an ``OffloadableField`` so the + ``ActivityInOutOffloadingInterceptor`` can stream the payload to blob + storage when its serialized size exceeds the configured threshold. + """ + + payload: OffloadableField[PipelexPipeRunInput] + + +class PipelexPipeRunOutputOffloaded(OffloadableModel): + """Offload-capable variant of ``PipelexPipeRunOutput``. + + Mirrors ``PipelexPipeRunInputOffloaded`` for the return path. + """ + + payload: OffloadableField[PipelexPipeRunOutput] + + @activity( start_to_close_timeout=timedelta(minutes=10), retry_policy_max_attempts=3, @@ -38,3 +69,26 @@ async def pipelex_run_pipe(input_payload: PipelexPipeRunInput) -> PipelexPipeRun call ``run_pipe_via_bridge`` directly from your own ``@activity`` (Tier 2). """ return await run_pipe_via_bridge(input_payload) + + +@activity( + start_to_close_timeout=timedelta(minutes=10), + retry_policy_max_attempts=3, +) +async def pipelex_run_pipe_offloaded( + input_payload: PipelexPipeRunInputOffloaded, +) -> PipelexPipeRunOutputOffloaded: + """Run a Pipelex pipe with offload-capable boundary types. + + Same semantics as ``pipelex_run_pipe`` but the input/output are wrapped + in ``OffloadableField``. To actually offload payloads to blob storage, + the worker must be configured with ``ActivityInOutOffloadingInterceptor`` + pointing at S3/GCS/Azure (see Mistral's + ``workflow_activity_offloading`` example). Without that interceptor, the + payload still rides inline through Temporal and offloading is a no-op. + """ + pipe_input = input_payload.payload.get_value() + pipe_output = await run_pipe_via_bridge(pipe_input) + return PipelexPipeRunOutputOffloaded( + payload=OffloadableField(value=pipe_output), + ) diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py new file mode 100644 index 000000000..40b13a243 --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py @@ -0,0 +1,117 @@ +"""Layer-2 integration test: ``pipelex_run_pipe_offloaded`` activity end-to-end. + +Verifies that the ``OffloadableField``-based variant correctly wraps and +unwraps Pipelex payloads through a Mistral Workflows activity. Skipped when +``mistralai-workflows`` is not installed. + +This test does NOT exercise Mistral's actual blob-storage offloading path — +that requires worker-level interceptor configuration with real S3/GCS/Azure +storage. It exercises the wrapping/unwrapping shape (the part Pipelex owns) +so users can confidently configure the offloading interceptor on their own +workers without surprises at the model boundary. +""" + +from typing import Any + +import pytest +import pytest_asyncio + +mistralai_workflows = pytest.importorskip("mistralai.workflows") + +from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 +from mistralai.workflows.core.encoding.fields_offloader import OffloadableField # noqa: E402 +from mistralai.workflows.testing import create_test_worker # noqa: E402 # pyright: ignore[reportUnknownVariableType] +from temporalio.common import SearchAttributeKey # noqa: E402 +from temporalio.testing import WorkflowEnvironment # noqa: E402 + +with mistralai_workflows.workflow.unsafe.imports_passed_through(): + from pipelex.plugins.mistralai_workflows.activities import ( + PipelexPipeRunInputOffloaded, + PipelexPipeRunOutputOffloaded, + pipelex_run_pipe_offloaded, + ) + from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + ) + from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" +TEST_TASK_QUEUE = "pipelex-mistralai-workflows-offloaded-test" + +# Larger than Mistral's default offloading threshold (typically a few KiB), +# small enough that the in-process Temporal test server still accepts it +# inline. Keeps the test self-contained while exercising a non-trivial +# payload through the OffloadableField wrapper. +LARGE_INPUT_SIZE_BYTES = 200 * 1024 + + +@mistralai_workflows.workflow.define( + name="pipelex-bridge-offloaded-test-workflow", + enforce_determinism=False, +) +class PipelexBridgeOffloadedTestWorkflow: + @mistralai_workflows.workflow.entrypoint + async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: + inner = PipelexPipeRunInput.model_validate(payload_dict) + wrapped = PipelexPipeRunInputOffloaded(payload=OffloadableField(value=inner)) + result: PipelexPipeRunOutputOffloaded = await pipelex_run_pipe_offloaded(wrapped) + unwrapped: PipelexPipeRunOutput = result.payload.get_value() + return unwrapped + + +@pytest.fixture(scope="module", autouse=True) +def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] + """Pin Mistral's global task_queue config to our test queue (see test_activities_direct.py).""" + original = mistralai_config.temporal.task_queue + mistralai_config.temporal.task_queue = TEST_TASK_QUEUE + try: + yield + finally: + mistralai_config.temporal.task_queue = original + + +@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] +async def workflow_env(): + env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] + search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], + ) + try: + yield env + finally: + await env.shutdown() + + +@pytest.mark.asyncio(loop_scope="class") +class TestPipelexRunPipeOffloadedActivity: + async def test_offloaded_activity_round_trips_large_payload( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + large_text = "x" * LARGE_INPUT_SIZE_BYTES + payload = PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": large_text}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + + async with create_test_worker( + workflow_env, + workflows=[PipelexBridgeOffloadedTestWorkflow], + activities=[pipelex_run_pipe_offloaded], + task_queue=TEST_TASK_QUEUE, + ): + result_dict = await workflow_env.client.execute_workflow( + PipelexBridgeOffloadedTestWorkflow.run, + {"payload_dict": payload.model_dump(mode="json")}, + id="pipelex-bridge-offloaded-test-workflow", + task_queue=TEST_TASK_QUEUE, + ) + + result = PipelexPipeRunOutput.model_validate(result_dict) + assert result.is_completed is True + assert result.workflow_id is None + assert result.main_stuff_name is not None + echoed_text = result.output_dict["root"][result.main_stuff_name]["content"]["text"] + assert echoed_text == large_text diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py index 77155d49e..7c69b0dce 100644 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py @@ -83,3 +83,36 @@ async def test_direct_mode_uses_caller_pipeline_run_id( assert result.is_completed is True assert result.pipeline_run_id == caller_run_id + + async def test_direct_mode_dynamic_concept_round_trips_via_library_crate_dump( + self, + bridge_test_library: str, + ) -> None: + """A concept with an inline structure round-trips through ``library_crate_dump``. + + ``EchoEnvelope`` is defined inline in the bridge_test bundle. The bridge + dehydrates the library to a JSON-safe crate dump, opens a per-call + scoped library on the receiving side, and re-hydrates the concept so + ``PipeCompose`` can construct a ``StructuredContent`` matching the + dynamic shape. + """ + envelope_pipe_ref = "mistralai_workflows_bridge_test.bridge_envelope_pipe" + crate = get_library_manager().get_crate(library_id=bridge_test_library) + assert crate is not None + crate_dump: dict[str, Any] = crate.model_dump(mode="json") + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=envelope_pipe_ref, + inputs={"input_text": "wrapped"}, + library_crate_dump=crate_dump, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.main_stuff_name is not None + main_stuff = result.output_dict["root"][result.main_stuff_name] + content = main_stuff["content"] + assert content["text"] == "wrapped" + assert content["origin"] == "mistralai_workflows_bridge" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds index 656f83946..20fa07114 100644 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds @@ -1,6 +1,13 @@ domain = "mistralai_workflows_bridge_test" description = "Test pipes for the mistralai_workflows plugin bridge" +[concept.EchoEnvelope] +description = "Custom dynamic concept used to exercise library_crate_dump round-trip for inline-structured concepts." + +[concept.EchoEnvelope.structure] +text = { type = "text", required = true, description = "The echoed text" } +origin = { type = "text", required = true, description = "Origin marker for the echo" } + [pipe.bridge_func_pipe] type = "PipeFunc" description = "Echoes the input text back as output (DIRECT mode only — PipeFunc is not Temporal-compatible)" @@ -13,3 +20,13 @@ description = "Echoes the input_text via a Jinja2 template (Temporal-compatible) inputs = { input_text = "Text" } output = "Text" template = "{{ input_text.text }}" + +[pipe.bridge_envelope_pipe] +type = "PipeCompose" +description = "Composes the input_text into a structured EchoEnvelope, exercising dynamic-concept round-trip via library_crate_dump" +inputs = { input_text = "Text" } +output = "EchoEnvelope" + +[pipe.bridge_envelope_pipe.construct] +text = { from = "input_text.text" } +origin = "mistralai_workflows_bridge" From 252cc1972ac58588f534f498f8340e98b3d586ef Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 02:16:29 +0200 Subject: [PATCH 05/16] Implement streaming support for Pipelex in Mistral Workflows - Added `pipelex_run_pipe_streaming` activity to enable real-time progress tracking of Pipelex pipes within Mistral Workflows, emitting lifecycle events (`CustomTaskStarted`, `CustomTaskInProgress`, `CustomTaskCompleted`, `CustomTaskFailed`). - Enhanced `DIRECT` execution mode to provide per-step event updates, allowing detailed observability of pipe execution. - Updated `CHANGELOG.md` to document the new streaming activity and its features. - Created integration tests for the streaming activity to validate event emissions and functionality. - Improved documentation with examples for using the new streaming variant in workflows. --- CHANGELOG.md | 1 + TODOS.md | 53 ++++--- .../mistralai-workflows-plugin.md | 3 +- .../mistralai-workflows-recipes.md | 50 ++++++ .../plugins/mistralai_workflows/streaming.py | 97 ++++++++++++ .../test_activities_streaming.py | 145 ++++++++++++++++++ 6 files changed, 324 insertions(+), 25 deletions(-) create mode 100644 pipelex/plugins/mistralai_workflows/streaming.py create mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d99c950dd..b12898163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. A second activity `pipelex_run_pipe_offloaded` (with `PipelexPipeRunInputOffloaded` / `PipelexPipeRunOutputOffloaded` boundary types wrapped in `OffloadableField`) lets users plug into Mistral's `ActivityInOutOffloadingInterceptor` for payloads that exceed Temporal's per-event size limit. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. +- **Streaming variant of the bridge activity** — `pipelex.plugins.mistralai_workflows.streaming.pipelex_run_pipe_streaming` wraps `run_pipe_via_bridge` in a single Mistral `Task` (`custom_task_type="pipelex.pipe_run"`) so subscribers get `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed` lifecycle events for each pipe run. The silent path (`pipelex_run_pipe`) is unchanged — opt into the streaming activity only when you need observability. Per-step granularity (one event per pipe sub-step, driven by Pipelex's `report_delegate`) is on the roadmap as Phase 2.1. ## [v0.26.4] - 2026-05-06 diff --git a/TODOS.md b/TODOS.md index 6440a3fe0..eb852e503 100644 --- a/TODOS.md +++ b/TODOS.md @@ -20,8 +20,8 @@ user-facing reference lives at | 1.2 | `TEMPORAL_BLOCKING` + `TEMPORAL_FIRE_AND_FORGET` modes | ✅ done | | 1.3 | Docs + CHANGELOG (mkdocs nav wired) | ✅ done¹ | | 1.5 | Large-payload `pipelex_run_pipe_offloaded` variant | ✅ done | -| 2.0 | Streaming v1 — one Mistral `task()` per activity | ⏳ next | -| 2.1 | Streaming v2 — per-step `task.update()` (conditional) | ⏳ after 2.0 | +| 2.0 | Streaming v1 — one Mistral `task()` per activity | ✅ done | +| 2.1 | Streaming v2 — per-step `task.update()` (conditional) | ⏳ next, conditional on demand | ¹ Cookbook example deferred to a follow-up PR in the sibling `pipelex-cookbook/` repo (suggested entry: @@ -90,7 +90,7 @@ pipelex/plugins/mistralai_workflows/ ├── bridge.py # framework-agnostic core (NO mistralai imports) ├── bootstrap.py # ensure_pipelex_booted() + DI helper ├── activities.py # Tier-1 wrappers + offloaded variant -└── streaming.py # Phase 2 — NOT YET CREATED +└── streaming.py # Phase 2.0 — pipelex_run_pipe_streaming sibling activity tests/unit/pipelex/plugins/mistralai_workflows/ ├── test_input_models.py # boundary BaseModels @@ -104,6 +104,7 @@ tests/integration/pipelex/plugins/mistralai_workflows/ ├── test_bridge_direct.py # layer 1 (no optional dep) ├── test_activities_direct.py # layer 2 (Mistral test worker) ├── test_activities_offloaded.py # layer 2 — offloaded variant +├── test_activities_streaming.py # layer 2 — Phase 2.0 streaming variant ├── test_bridge_temporal_blocking.py # layer 3 (+ temporal extra) └── test_bridge_temporal_fire_and_forget.py # layer 3 (+ temporal extra) ``` @@ -122,6 +123,11 @@ from pipelex.plugins.mistralai_workflows.bridge import ( ) from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted +from pipelex.plugins.mistralai_workflows.streaming import ( + pipelex_run_pipe_streaming, + PipelexPipeRunStreamingState, + PIPELEX_PIPE_RUN_TASK_TYPE, +) ``` --- @@ -191,32 +197,31 @@ Goal: surface live progress events from Pipelex pipes through Mistral's `task()` event API so users can subscribe via `create_capturing_mock_events_client` and friends. -### Phase 2.0 — One task per activity (started / completed / failed) +### Phase 2.0 — One task per activity (started / completed / failed) ✅ A single Mistral task wraps the whole activity body — no per-step granularity. Cheapest path to "the user sees something happen." -- [ ] Create `pipelex/plugins/mistralai_workflows/streaming.py`. Top-of- - file imports `mistralai.workflows` — reuse the same optional-dep - guard pattern as `activities.py`. -- [ ] **Decision (open):** wrap `pipelex_run_pipe` directly, OR ship a - sibling activity `pipelex_run_pipe_streaming`? - - Wrapping the existing one is one fewer activity to register but - forces every caller to pay the Mistral `task()` overhead. - - A sibling activity keeps the silent path silent. Leaning toward - **sibling**; revisit when we measure the overhead. -- [ ] Inside the wrapper: `async with workflows.task(...) as t:`, then: - - emit `started` with `pipe_code` + `pipeline_run_id` - - emit `completed` with an output summary on success - - emit `failed` with exception details on error - - catch the same specific exceptions `bridge.py::_run_*` already - catches (`PipeRunError`, `PipeJobError`, `PipeRouterError`, - `PipeExecutionError`, `PipelineExecutionError`) — never catch - generic `Exception` per Pipelex standards. -- [ ] New layer-4 integration test +- [x] Created `pipelex/plugins/mistralai_workflows/streaming.py`. Reuses + the same optional-dep guard pattern as `activities.py` (top-level + `try`/`except ImportError` re-raising `MistralWorkflowsNotInstalledError`). +- [x] **Decision: sibling activity** `pipelex_run_pipe_streaming`. Reasons: + - Keeps the silent path silent — Tier-1 users without observability + needs don't pay event publishing overhead. + - Mirrors the `pipelex_run_pipe_offloaded` sibling pattern. + - Workers register only the variant they actually use. +- [x] Inside the activity: `async with Task[PipelexPipeRunStreamingState](...)` + then call `run_pipe_via_bridge`, then `update_state` to `phase="completed"`. + `Task.__aexit__` automatically emits `CustomTaskFailed` on exception + and the original exception propagates — no extra `try`/`except` + needed (per Pipelex "don't catch Exception speculatively" rule). +- [x] Layer-2 integration test `tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py` - using `create_test_worker_with_events` + - `create_capturing_mock_events_client`. Re-apply gotchas §4.1–4.4. + uses `create_test_worker_with_events` + `EventContext` + a + `create_capturing_mock_events_client` to assert that exactly one + `CustomTaskStarted`, ≥1 `CustomTaskInProgress`, and one + `CustomTaskCompleted` are emitted with the right `custom_task_type` + (`pipelex.pipe_run`) and payload shape. ### Phase 2.1 — Per-step granularity (only if 2.0 is too coarse) diff --git a/docs/under-the-hood/mistralai-workflows-plugin.md b/docs/under-the-hood/mistralai-workflows-plugin.md index a06a876c8..5890bc115 100644 --- a/docs/under-the-hood/mistralai-workflows-plugin.md +++ b/docs/under-the-hood/mistralai-workflows-plugin.md @@ -25,7 +25,7 @@ For the `TEMPORAL_BLOCKING` and `TEMPORAL_FIRE_AND_FORGET` execution modes, also pip install 'pipelex[mistralai-workflows,temporal]' ``` -The framework-agnostic core (`bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`) is importable on a venv that does NOT have `mistralai-workflows` installed. The optional-dep guard fires only when you import `pipelex.plugins.mistralai_workflows.activities` (or `streaming` once shipped). +The framework-agnostic core (`bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`) is importable on a venv that does NOT have `mistralai-workflows` installed. The optional-dep guard fires only when you import `pipelex.plugins.mistralai_workflows.activities` or `pipelex.plugins.mistralai_workflows.streaming`. --- @@ -45,6 +45,7 @@ from pipelex.plugins.mistralai_workflows.bootstrap import ( ensure_pipelex_booted, get_pipelex_dependency, ) +from pipelex.plugins.mistralai_workflows.streaming import pipelex_run_pipe_streaming ``` --- diff --git a/docs/under-the-hood/mistralai-workflows-recipes.md b/docs/under-the-hood/mistralai-workflows-recipes.md index cf6d808ea..023ab5af4 100644 --- a/docs/under-the-hood/mistralai-workflows-recipes.md +++ b/docs/under-the-hood/mistralai-workflows-recipes.md @@ -107,6 +107,56 @@ Trade-off: offloaded payloads live in the blob storage you configure (S3 by defa --- +## Live progress events — `pipelex_run_pipe_streaming` + +When a UI subscribes to a Mistral Workflow execution and needs to "see something happen" while a Pipelex pipe runs, use the streaming variant. It wraps the same bridge call in a single Mistral `Task` whose lifecycle (`CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed`) is published to whatever events client your worker is configured with. + +```python +from mistralai import workflows + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, +) +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.plugins.mistralai_workflows.streaming import pipelex_run_pipe_streaming + + +@workflows.workflow.define(name="extract-invoice-streaming-flow") +class ExtractInvoiceStreamingFlow: + @workflows.workflow.entrypoint + async def run(self, doc_url: str) -> dict: + result: PipelexPipeRunOutput = await pipelex_run_pipe_streaming( + PipelexPipeRunInput( + pipe_code="finance.extract_invoice", + inputs={"doc_url": doc_url}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + return result.output_dict + + +await workflows.run_worker( + [ExtractInvoiceStreamingFlow], + activities=[pipelex_run_pipe_streaming], +) +``` + +The events carry a small JSON payload identifying the run: + +| Event | Payload | +| ------------------------ | ----------------------------------------------------------------------------- | +| `CustomTaskStarted` | `phase="started"`, `pipe_code`, `execution_mode`, `pipeline_run_id` (if set) | +| `CustomTaskInProgress` | JSON-patch transition to `phase="completed"` with `pipeline_run_id` filled | +| `CustomTaskCompleted` | Final state — same shape as the InProgress payload | +| `CustomTaskFailed` | The original exception message (emitted by `Task.__aexit__` on failure) | + +`custom_task_type` is always `"pipelex.pipe_run"`, so subscribers can filter on it without parsing the payload. + +For the silent path (no observability, no event publishing overhead per activity) keep using `pipelex_run_pipe` — the streaming variant is opt-in. Per-step granularity (one event per pipe sub-step, driven by Pipelex's `report_delegate`) is on the roadmap as Phase 2.1. + +--- + ## Tier 2 — helper inside your own typed activity Wrap `run_pipe_via_bridge` in your own `@activity`-decorated function so you control all activity options and the input/output types. diff --git a/pipelex/plugins/mistralai_workflows/streaming.py b/pipelex/plugins/mistralai_workflows/streaming.py new file mode 100644 index 000000000..6fb8d617c --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/streaming.py @@ -0,0 +1,97 @@ +"""Phase 2.0 — streaming variant of the Pipelex bridge activity. + +Wraps :func:`pipelex.plugins.mistralai_workflows.bridge.run_pipe_via_bridge` +in a single Mistral Workflows ``Task`` so subscribers can observe the pipe +run through ``CustomTaskStarted`` / ``CustomTaskInProgress`` / +``CustomTaskCompleted`` / ``CustomTaskFailed`` events. + +Phase 2.0 emits exactly two state transitions per call: ``started`` (on entry) +and ``completed`` (after the bridge returns). On exception, ``Task.__aexit__`` +emits ``CustomTaskFailed`` automatically and the original exception +propagates. Per-step granularity (mapping Pipelex's ``report_delegate`` events +to ``Task.update_state`` calls) is Phase 2.1 and lives in this same module +when added. + +Importing this module triggers the optional-dep guard: if +``mistralai-workflows`` is not installed, the import fails fast with a +``MistralWorkflowsNotInstalledError`` carrying install instructions. The +sibling ``activities`` module follows the same pattern. +""" + +from datetime import timedelta + +from pydantic import BaseModel, ConfigDict + +from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + run_pipe_via_bridge, +) +from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError + +try: + from mistralai.workflows import activity + from mistralai.workflows.core.task import Task +except ImportError as exc: + msg = ( + "The 'mistralai-workflows' optional dependency is required to use " + "pipelex.plugins.mistralai_workflows.streaming. " + "Install with: pip install 'pipelex[mistralai-workflows]'" + ) + raise MistralWorkflowsNotInstalledError(msg) from exc + + +PIPELEX_PIPE_RUN_TASK_TYPE = "pipelex.pipe_run" + + +class PipelexPipeRunStreamingState(BaseModel): + """Observable state surfaced through Mistral's Task API for a Pipelex pipe run. + + Phase 2.0 only writes ``started`` (on entry) and ``completed`` (after the + bridge returns successfully). On failure, ``Task.__aexit__`` emits + ``CustomTaskFailed`` with the exception message — no extra state write + is needed and the original exception is preserved. + """ + + model_config = ConfigDict(extra="forbid") + + phase: str + pipe_code: str + execution_mode: str + pipeline_run_id: str | None = None + main_stuff_name: str | None = None + + +@activity( + start_to_close_timeout=timedelta(minutes=10), + retry_policy_max_attempts=3, +) +async def pipelex_run_pipe_streaming(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: + """Streaming variant of ``pipelex_run_pipe``. + + Same semantics as :func:`pipelex_run_pipe` but wraps the bridge call in a + single Mistral ``Task`` whose lifecycle (``started``, ``in_progress``, + ``completed`` / ``failed``) is published to whichever events client the + worker is configured with. For the silent path (no observability needed) + use ``pipelex_run_pipe`` instead — the streaming variant adds a small + constant overhead per activity for the lifecycle events. + """ + initial_state = PipelexPipeRunStreamingState( + phase="started", + pipe_code=input_payload.pipe_code, + execution_mode=input_payload.execution_mode, + pipeline_run_id=input_payload.pipeline_run_id, + ) + async with Task[PipelexPipeRunStreamingState]( + type=PIPELEX_PIPE_RUN_TASK_TYPE, + state=initial_state, + ) as streaming_task: + output = await run_pipe_via_bridge(input_payload) + await streaming_task.update_state( + { + "phase": "completed", + "pipeline_run_id": output.pipeline_run_id, + "main_stuff_name": output.main_stuff_name, + } + ) + return output diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py new file mode 100644 index 000000000..720ab3e54 --- /dev/null +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py @@ -0,0 +1,145 @@ +"""Layer-2 integration test: ``pipelex_run_pipe_streaming`` activity end-to-end. + +Spins an in-process Temporal test environment plus a Mistral test worker +configured with the ``EventInterceptor``, runs a workflow that invokes +``pipelex_run_pipe_streaming``, and asserts that the lifecycle events +(``CustomTaskStarted`` → ``CustomTaskInProgress`` → ``CustomTaskCompleted``) +were published with the expected ``custom_task_type`` and payload shape. + +Skipped when ``mistralai-workflows`` is not installed. +""" + +from typing import Any + +import pytest +import pytest_asyncio + +mistralai_workflows = pytest.importorskip("mistralai.workflows") + +from mistralai.workflows.core._events.event_context import EventContext # noqa: E402, PLC2701 +from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 +from mistralai.workflows.protocol.v1.events import ( # noqa: E402 + CustomTaskCompleted, + CustomTaskInProgress, + CustomTaskStarted, + WorkflowEvent, +) +from mistralai.workflows.testing import ( # noqa: E402 + create_capturing_mock_events_client, + create_test_worker_with_events, # pyright: ignore[reportUnknownVariableType] +) +from temporalio.common import SearchAttributeKey # noqa: E402 +from temporalio.testing import WorkflowEnvironment # noqa: E402 + +with mistralai_workflows.workflow.unsafe.imports_passed_through(): + from pipelex.plugins.mistralai_workflows.bridge import ( + PipelexPipeRunInput, + PipelexPipeRunOutput, + ) + from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode + from pipelex.plugins.mistralai_workflows.streaming import ( + PIPELEX_PIPE_RUN_TASK_TYPE, + pipelex_run_pipe_streaming, + ) + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" +TEST_TASK_QUEUE = "pipelex-mistralai-workflows-streaming-test" + + +@mistralai_workflows.workflow.define( + name="pipelex-bridge-streaming-test-workflow", + enforce_determinism=False, +) +class PipelexBridgeStreamingTestWorkflow: + @mistralai_workflows.workflow.entrypoint + async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: + payload = PipelexPipeRunInput.model_validate(payload_dict) + output: PipelexPipeRunOutput = await pipelex_run_pipe_streaming(payload) + return output + + +@pytest.fixture(scope="module", autouse=True) +def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] + """Pin Mistral's global task_queue to our test queue (see test_activities_direct.py).""" + original = mistralai_config.temporal.task_queue + mistralai_config.temporal.task_queue = TEST_TASK_QUEUE + try: + yield + finally: + mistralai_config.temporal.task_queue = original + + +@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] +async def workflow_env(): + env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] + search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], + ) + try: + yield env + finally: + await env.shutdown() + + +@pytest.mark.asyncio(loop_scope="class") +class TestPipelexRunPipeStreamingActivity: + async def test_workflow_emits_custom_task_lifecycle_events( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + captured_events: list[WorkflowEvent] = [] + mock_events_client = create_capturing_mock_events_client(captured_events) + + payload = PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "via streaming activity"}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + + async with ( + EventContext(events_client=mock_events_client), + create_test_worker_with_events( + workflow_env, + workflows=[PipelexBridgeStreamingTestWorkflow], + activities=[pipelex_run_pipe_streaming], + task_queue=TEST_TASK_QUEUE, + ), + ): + result_dict = await workflow_env.client.execute_workflow( + PipelexBridgeStreamingTestWorkflow.run, + {"payload_dict": payload.model_dump(mode="json")}, + id="pipelex-bridge-streaming-test-workflow", + task_queue=TEST_TASK_QUEUE, + ) + + result = PipelexPipeRunOutput.model_validate(result_dict) + assert result.is_completed is True + assert result.main_stuff_name is not None + assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "via streaming activity" + + custom_task_events = [ + event + for event in captured_events + if isinstance(event, (CustomTaskStarted, CustomTaskInProgress, CustomTaskCompleted)) + and event.attributes.custom_task_type == PIPELEX_PIPE_RUN_TASK_TYPE + ] + + # Expect exactly one Started, at least one InProgress (the "completed" state update), + # and one Completed event for the pipe-run task. + started_events = [event for event in custom_task_events if isinstance(event, CustomTaskStarted)] + in_progress_events = [event for event in custom_task_events if isinstance(event, CustomTaskInProgress)] + completed_events = [event for event in custom_task_events if isinstance(event, CustomTaskCompleted)] + + assert len(started_events) == 1, f"expected 1 CustomTaskStarted, got {len(started_events)}" + assert len(in_progress_events) >= 1, f"expected >=1 CustomTaskInProgress, got {len(in_progress_events)}" + assert len(completed_events) == 1, f"expected 1 CustomTaskCompleted, got {len(completed_events)}" + + started_payload = started_events[0].attributes.payload.value + assert started_payload["pipe_code"] == PIPE_REF + assert started_payload["phase"] == "started" + assert started_payload["execution_mode"] == PipelexExecutionMode.DIRECT + + completed_payload = completed_events[0].attributes.payload.value + assert completed_payload["phase"] == "completed" + assert completed_payload["pipeline_run_id"] == result.pipeline_run_id + assert completed_payload["main_stuff_name"] == result.main_stuff_name From d0cfa6729cc4c7563dd101168c17dbd247963c20 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 02:16:39 +0200 Subject: [PATCH 06/16] Enhance streaming capabilities for Pipelex in Mistral Workflows - Implemented per-step event updates for the `pipelex_run_pipe_streaming` activity in `DIRECT` execution mode, allowing detailed observability of pipe execution through `CustomTaskInProgress` events. - Updated `CHANGELOG.md` to reflect the new features and improvements in streaming support. - Enhanced integration tests to validate the emission of per-step events during multi-step pipe executions. - Improved documentation with examples for utilizing the new streaming features in workflows. --- CHANGELOG.md | 3 +- TODOS.md | 56 +++- .../mistralai-workflows-recipes.md | 24 +- pipelex/plugins/mistralai_workflows/bridge.py | 28 +- .../plugins/mistralai_workflows/streaming.py | 163 +++++++++- .../streaming_event_forwarder.py | 284 ++++++++++++++++++ .../test_activities_streaming.py | 116 ++++++- .../test_data/bridge_test.mthds | 24 ++ 8 files changed, 659 insertions(+), 39 deletions(-) create mode 100644 pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b12898163..1f2d28992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Added - **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. A second activity `pipelex_run_pipe_offloaded` (with `PipelexPipeRunInputOffloaded` / `PipelexPipeRunOutputOffloaded` boundary types wrapped in `OffloadableField`) lets users plug into Mistral's `ActivityInOutOffloadingInterceptor` for payloads that exceed Temporal's per-event size limit. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. -- **Streaming variant of the bridge activity** — `pipelex.plugins.mistralai_workflows.streaming.pipelex_run_pipe_streaming` wraps `run_pipe_via_bridge` in a single Mistral `Task` (`custom_task_type="pipelex.pipe_run"`) so subscribers get `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed` lifecycle events for each pipe run. The silent path (`pipelex_run_pipe`) is unchanged — opt into the streaming activity only when you need observability. Per-step granularity (one event per pipe sub-step, driven by Pipelex's `report_delegate`) is on the roadmap as Phase 2.1. +- **Streaming variant of the bridge activity** — `pipelex.plugins.mistralai_workflows.streaming.pipelex_run_pipe_streaming` wraps `run_pipe_via_bridge` in a single Mistral `Task` (`custom_task_type="pipelex.pipe_run"`) so subscribers get `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed` lifecycle events for each pipe run. The silent path (`pipelex_run_pipe`) is unchanged — opt into the streaming activity only when you need observability. +- **Per-step streaming for `DIRECT` mode** — when called with `PipelexExecutionMode.DIRECT`, `pipelex_run_pipe_streaming` now also emits one `CustomTaskInProgress` event per Pipelex pipe boundary (PipeStartEvent / PipeEndSuccessEvent / PipeEndErrorEvent), surfacing `current_step_pipe_code`, `current_step_node_id`, `started_steps`, `completed_steps`, and `last_output_stuff_name` on the streaming state. Implemented via a queue-backed `EventLogProtocol` injected into a per-call `GraphTracerManager` tracer plus an asyncio forwarder that translates trace events into `Task.update_state(...)` calls. `TEMPORAL_BLOCKING` / `TEMPORAL_FIRE_AND_FORGET` keep the previous single-pair semantics. ## [v0.26.4] - 2026-05-06 diff --git a/TODOS.md b/TODOS.md index eb852e503..3168bc18d 100644 --- a/TODOS.md +++ b/TODOS.md @@ -21,7 +21,7 @@ user-facing reference lives at | 1.3 | Docs + CHANGELOG (mkdocs nav wired) | ✅ done¹ | | 1.5 | Large-payload `pipelex_run_pipe_offloaded` variant | ✅ done | | 2.0 | Streaming v1 — one Mistral `task()` per activity | ✅ done | -| 2.1 | Streaming v2 — per-step `task.update()` (conditional) | ⏳ next, conditional on demand | +| 2.1 | Streaming v2 — per-step `task.update()` (DIRECT only) | ✅ done | ¹ Cookbook example deferred to a follow-up PR in the sibling `pipelex-cookbook/` repo (suggested entry: @@ -223,21 +223,45 @@ granularity. Cheapest path to "the user sees something happen." `CustomTaskCompleted` are emitted with the right `custom_task_type` (`pipelex.pipe_run`) and payload shape. -### Phase 2.1 — Per-step granularity (only if 2.0 is too coarse) - -- [ ] Subscribe to Pipelex's `report_delegate` event stream from inside - the activity. -- [ ] Map Pipelex events to Mistral `task.update(...)` calls: - - pipe sub-step started → `in_progress` with description - - stuff added to working memory → `in_progress` with new key - - pipe step completed → progress % -- [ ] Forwarder side-task drains the event log and terminates cleanly on - both success and failure. Use `try` / `finally` (NOT `try` / - `except Exception`) for cleanup, per Pipelex standards. -- [ ] Test asserts per-step events emitted in correct order for a - multi-step pipe. The current `bridge_test.mthds` has only single- - step pipes — Phase 2.1 needs a multi-step fixture (e.g. a - `PipeSequence` chaining two `PipeCompose`s). +### Phase 2.1 — Per-step granularity (DIRECT mode only) ✅ + +- [x] Subscribe to Pipelex's trace event channel from inside the + activity. Implemented as a queue-backed `EventLogProtocol` + (`QueueEventLog` in `streaming_event_forwarder.py`) injected into a + per-call `GraphTracerManager` tracer. Note: `ReportingProtocol` + itself has no observer methods — the right abstraction is the + event log, not the reporting delegate. +- [x] Map Pipelex events to Mistral `task.update_state(...)` calls: + - `PipeStartEvent` → `in_progress` with `current_step_pipe_code`, + `current_step_node_id`, `last_event_kind="pipe_start"`, + `started_steps`. + - `PipeEndSuccessEvent` → `in_progress` with + `last_event_kind="pipe_end_success"`, `completed_steps`, + `last_output_stuff_name`. + - `PipeEndErrorEvent` → `in_progress` with + `last_event_kind="pipe_end_error"`. The activity's + `Task.__aexit__` then emits `CustomTaskFailed` with the + propagated exception. + - Other `TraceEventKind`s (edges, batch fan-out, controller + outputs, execution data, usage reports) intentionally suppressed + — too noisy for state updates, already captured by Pipelex's + own reporting / graph infrastructure. +- [x] Forwarder side-task drains the event log and terminates cleanly + on both success and failure. Uses `try` / `finally` (no + `except Exception`); the forwarder is fully drained *before* + writing the final `phase="completed"` state so the snapshot + reflects the right phase. +- [x] Multi-step test fixture: `bridge_sequence_pipe` (PipeSequence) + chaining `bridge_seq_step_one` and `bridge_seq_step_two` + (PipeCompose). Test asserts ≥3 pipe_start + ≥3 pipe_end_success + `CustomTaskInProgress` events with `started_steps` monotonic + `[1, 2, 3]` and per-step pipe codes in declaration order. + +Scope: DIRECT mode only. TEMPORAL_BLOCKING / TEMPORAL_FIRE_AND_FORGET +keep Phase 2.0 single-pair semantics — per-step streaming across the +Temporal worker boundary would need cross-process tee logic on top of +the existing `pipeline_run_setup` event log infra, deferred until demand +surfaces. --- diff --git a/docs/under-the-hood/mistralai-workflows-recipes.md b/docs/under-the-hood/mistralai-workflows-recipes.md index 023ab5af4..d388bc30a 100644 --- a/docs/under-the-hood/mistralai-workflows-recipes.md +++ b/docs/under-the-hood/mistralai-workflows-recipes.md @@ -147,13 +147,31 @@ The events carry a small JSON payload identifying the run: | Event | Payload | | ------------------------ | ----------------------------------------------------------------------------- | | `CustomTaskStarted` | `phase="started"`, `pipe_code`, `execution_mode`, `pipeline_run_id` (if set) | -| `CustomTaskInProgress` | JSON-patch transition to `phase="completed"` with `pipeline_run_id` filled | -| `CustomTaskCompleted` | Final state — same shape as the InProgress payload | +| `CustomTaskInProgress` | JSON-patch updates: per-step boundaries (DIRECT mode) and the final transition to `phase="completed"` | +| `CustomTaskCompleted` | Final full-state snapshot with `phase="completed"` and `main_stuff_name` | | `CustomTaskFailed` | The original exception message (emitted by `Task.__aexit__` on failure) | `custom_task_type` is always `"pipelex.pipe_run"`, so subscribers can filter on it without parsing the payload. -For the silent path (no observability, no event publishing overhead per activity) keep using `pipelex_run_pipe` — the streaming variant is opt-in. Per-step granularity (one event per pipe sub-step, driven by Pipelex's `report_delegate`) is on the roadmap as Phase 2.1. +### Per-step events for `DIRECT` mode + +When `execution_mode=PipelexExecutionMode.DIRECT`, the streaming activity publishes one `CustomTaskInProgress` event per Pipelex pipe boundary in addition to the final completed-state push. Each pipe-step event carries a JSON-patch update to the streaming state with the following fields: + +| Field | Description | +| --------------------------- | ---------------------------------------------------------------------------- | +| `phase` | `"in_progress"` (transition from `"started"` on the very first patch) | +| `current_step_pipe_code` | The pipe code for the most recent `PipeStartEvent` | +| `current_step_node_id` | The graph node id for that pipe | +| `last_event_kind` | `"pipe_start"` / `"pipe_end_success"` / `"pipe_end_error"` | +| `started_steps` | Cumulative count of pipe-step starts (1-indexed, monotonic) | +| `completed_steps` | Cumulative count of successful pipe-step completions | +| `last_output_stuff_name` | The output IOSpec name for the most recent successful step (or `null`) | + +A field only appears in a given `CustomTaskInProgress` JSON-patch when its value actually changed — for example, `last_event_kind` won't appear in two consecutive `pipe_start` events. Use `started_steps` / `completed_steps` (always changing) as discriminators when you need to count or order step events. + +`TEMPORAL_BLOCKING` and `TEMPORAL_FIRE_AND_FORGET` modes keep the simpler "one started + one completed" semantics — per-step streaming across the Temporal worker boundary is not supported in this release. + +For the silent path (no observability, no event publishing overhead per activity) keep using `pipelex_run_pipe` — the streaming variant is opt-in. --- diff --git a/pipelex/plugins/mistralai_workflows/bridge.py b/pipelex/plugins/mistralai_workflows/bridge.py index 61848fb2e..950aaf3c7 100644 --- a/pipelex/plugins/mistralai_workflows/bridge.py +++ b/pipelex/plugins/mistralai_workflows/bridge.py @@ -47,6 +47,7 @@ if TYPE_CHECKING: from pipelex.core.memory.working_memory import WorkingMemory from pipelex.core.pipes.pipe_output import PipeOutput + from pipelex.graph.graph_context import GraphContext from pipelex.pipe_run.pipe_job import PipeJob @@ -78,12 +79,23 @@ class PipelexPipeRunOutput(BaseModel): graph_spec_dump: dict[str, Any] | None = None -async def run_pipe_via_bridge(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: +async def run_pipe_via_bridge( + input_payload: PipelexPipeRunInput, + graph_context: GraphContext | None = None, +) -> PipelexPipeRunOutput: """Run a Pipelex pipe from inside a Mistral Workflows activity. Booting Pipelex on first call (no-op if already initialized); validating the input; opening a per-call scoped library if a ``library_crate_dump`` is provided; then dispatching to the requested execution mode. + + The optional ``graph_context`` is plumbed into ``JobMetadata`` so callers + (e.g. the streaming activity) that already opened a + ``GraphTracerManager`` tracer for this pipeline run get per-step trace + events flowing through the configured event log. ``graph_context`` is + only honored for ``DIRECT`` execution mode — TEMPORAL modes already + have their own event-log infrastructure via ``pipeline_run_setup`` and + a passed-in context would be ignored anyway. """ ensure_pipelex_booted() _validate_input(input_payload) @@ -92,7 +104,11 @@ async def run_pipe_via_bridge(input_payload: PipelexPipeRunInput) -> PipelexPipe delivery_assignment = _decode_delivery_assignment(input_payload.delivery_assignment_dump) async with _scoped_library_for_crate(library_crate): - pipe_job = build_pipe_job_from_input(input_payload=input_payload, library_crate=library_crate) + pipe_job = build_pipe_job_from_input( + input_payload=input_payload, + library_crate=library_crate, + graph_context=graph_context, + ) match input_payload.execution_mode: case PipelexExecutionMode.DIRECT: @@ -108,12 +124,19 @@ async def run_pipe_via_bridge(input_payload: PipelexPipeRunInput) -> PipelexPipe def build_pipe_job_from_input( input_payload: PipelexPipeRunInput, library_crate: LibraryCrate | None, + graph_context: GraphContext | None = None, ) -> PipeJob: """Hydrate a PipeJob from JSON-safe input. Looks up the pipe in the active library; the caller is responsible for making sure the active library contains the pipe (by passing a ``library_crate_dump`` or pre-loading the library at boot). + + The optional ``graph_context`` is plumbed into ``JobMetadata`` so a + caller (e.g. the streaming activity) that has already opened a + ``GraphTracerManager`` tracer for this pipeline run can have per-step + ``PipeStartEvent`` / ``PipeEndSuccessEvent`` events flow through the + pipe execution. When ``None``, no tracing happens (current default). """ pipe = get_required_pipe(pipe_code=input_payload.pipe_code) @@ -131,6 +154,7 @@ def build_pipe_job_from_input( job_metadata = JobMetadata( user_id=input_payload.user_id or OTelConstants.DEFAULT_USER_ID, pipeline_run_id=pipeline_run_id, + graph_context=graph_context, ) pipe_run_params = PipeRunParamsFactory.make_run_params() diff --git a/pipelex/plugins/mistralai_workflows/streaming.py b/pipelex/plugins/mistralai_workflows/streaming.py index 6fb8d617c..d1ee5f1d7 100644 --- a/pipelex/plugins/mistralai_workflows/streaming.py +++ b/pipelex/plugins/mistralai_workflows/streaming.py @@ -1,16 +1,17 @@ -"""Phase 2.0 — streaming variant of the Pipelex bridge activity. +"""Phase 2.1 — streaming variant of the Pipelex bridge activity. -Wraps :func:`pipelex.plugins.mistralai_workflows.bridge.run_pipe_via_bridge` -in a single Mistral Workflows ``Task`` so subscribers can observe the pipe -run through ``CustomTaskStarted`` / ``CustomTaskInProgress`` / -``CustomTaskCompleted`` / ``CustomTaskFailed`` events. +Wraps a Pipelex pipe run in a single Mistral Workflows ``Task`` so subscribers +can observe progress through ``CustomTaskStarted`` / ``CustomTaskInProgress`` +/ ``CustomTaskCompleted`` / ``CustomTaskFailed`` events. -Phase 2.0 emits exactly two state transitions per call: ``started`` (on entry) -and ``completed`` (after the bridge returns). On exception, ``Task.__aexit__`` -emits ``CustomTaskFailed`` automatically and the original exception -propagates. Per-step granularity (mapping Pipelex's ``report_delegate`` events -to ``Task.update_state`` calls) is Phase 2.1 and lives in this same module -when added. +Phase 2.0 emitted exactly two state transitions per call (``started`` / +``completed``). Phase 2.1 adds **per-step granularity** for ``DIRECT`` mode: +the activity opens a per-call ``GraphTracerManager`` tracer with a +queue-backed event log injected, and an asyncio forwarder drains the queue +into ``Task.update_state`` so each Pipelex pipe boundary produces a +``CustomTaskInProgress`` event. ``TEMPORAL_BLOCKING`` and +``TEMPORAL_FIRE_AND_FORGET`` keep Phase 2.0 behavior — per-step streaming +across the Temporal worker boundary is a future phase. Importing this module triggers the optional-dep guard: if ``mistralai-workflows`` is not installed, the import fails fast with a @@ -18,16 +19,31 @@ sibling ``activities`` module follows the same pattern. """ +from __future__ import annotations + +import asyncio from datetime import timedelta +from typing import Any +import shortuuid from pydantic import BaseModel, ConfigDict +from pipelex.graph.graph_tracer_manager import GraphTracerManager from pipelex.plugins.mistralai_workflows.bridge import ( PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge, ) from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError +from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode +from pipelex.plugins.mistralai_workflows.streaming_event_forwarder import ( + SHUTDOWN_SENTINEL, + QueueEventLog, + build_streaming_data_inclusion, + forward_events_to_task, + get_drain_timeout_seconds, + get_queue_max_size, +) try: from mistralai.workflows import activity @@ -47,10 +63,27 @@ class PipelexPipeRunStreamingState(BaseModel): """Observable state surfaced through Mistral's Task API for a Pipelex pipe run. - Phase 2.0 only writes ``started`` (on entry) and ``completed`` (after the - bridge returns successfully). On failure, ``Task.__aexit__`` emits - ``CustomTaskFailed`` with the exception message — no extra state write - is needed and the original exception is preserved. + Phase 2.0 fields (always present): + + - ``phase``: one of ``"started"`` / ``"in_progress"`` / ``"completed"``. + ``"failed"`` is not written explicitly — ``Task.__aexit__`` emits + ``CustomTaskFailed`` on exception and the original exception + propagates. + - ``pipe_code`` / ``execution_mode`` / ``pipeline_run_id`` / + ``main_stuff_name``: identifiers, set on ``started`` and refined on + ``completed``. + + Phase 2.1 fields (only populated for DIRECT mode runs that go through + ``pipelex_run_pipe_streaming``; remain at defaults for TEMPORAL modes): + + - ``current_step_pipe_code`` / ``current_step_node_id``: identify the + pipe boundary that just fired. + - ``last_event_kind``: ``"pipe_start"`` / ``"pipe_end_success"`` / + ``"pipe_end_error"`` — lets subscribers route on event type. + - ``started_steps`` / ``completed_steps``: cumulative counters, + monotonic, 1-indexed. + - ``last_output_stuff_name``: the IOSpec name of the most recent + successful step's output, or ``None`` if the step had no output spec. """ model_config = ConfigDict(extra="forbid") @@ -61,6 +94,13 @@ class PipelexPipeRunStreamingState(BaseModel): pipeline_run_id: str | None = None main_stuff_name: str | None = None + current_step_pipe_code: str | None = None + current_step_node_id: str | None = None + last_event_kind: str | None = None + started_steps: int = 0 + completed_steps: int = 0 + last_output_stuff_name: str | None = None + @activity( start_to_close_timeout=timedelta(minutes=10), @@ -75,13 +115,104 @@ async def pipelex_run_pipe_streaming(input_payload: PipelexPipeRunInput) -> Pipe worker is configured with. For the silent path (no observability needed) use ``pipelex_run_pipe`` instead — the streaming variant adds a small constant overhead per activity for the lifecycle events. + + For ``DIRECT`` execution mode, opens a per-call ``GraphTracerManager`` + tracer with an in-process queue-backed event log; spawns a forwarder + coroutine that translates each ``PipeStartEvent`` / + ``PipeEndSuccessEvent`` / ``PipeEndErrorEvent`` into a + ``Task.update_state`` call so subscribers see one + ``CustomTaskInProgress`` per pipe boundary. ``TEMPORAL_*`` modes keep + the Phase 2.0 single-pair behavior. """ + pipeline_run_id = input_payload.pipeline_run_id or shortuuid.uuid() + if input_payload.pipeline_run_id is None: + input_payload = input_payload.model_copy(update={"pipeline_run_id": pipeline_run_id}) + initial_state = PipelexPipeRunStreamingState( phase="started", pipe_code=input_payload.pipe_code, execution_mode=input_payload.execution_mode, - pipeline_run_id=input_payload.pipeline_run_id, + pipeline_run_id=pipeline_run_id, ) + + if input_payload.execution_mode is PipelexExecutionMode.DIRECT: + return await _run_streaming_with_per_step_events( + input_payload=input_payload, + pipeline_run_id=pipeline_run_id, + initial_state=initial_state, + ) + + return await _run_streaming_without_per_step_events( + input_payload=input_payload, + initial_state=initial_state, + ) + + +async def _run_streaming_with_per_step_events( + input_payload: PipelexPipeRunInput, + pipeline_run_id: str, + initial_state: PipelexPipeRunStreamingState, +) -> PipelexPipeRunOutput: + """DIRECT-mode streaming path — opens a tracer + forwarder for per-step events.""" + event_queue: asyncio.Queue[Any] = asyncio.Queue(maxsize=get_queue_max_size()) + queue_event_log = QueueEventLog(loop=asyncio.get_running_loop(), queue=event_queue) + tracer_manager = GraphTracerManager.get_or_create_instance() + graph_context = tracer_manager.open_tracer( + graph_id=pipeline_run_id, + data_inclusion=build_streaming_data_inclusion(), + pipeline_ref_domain=None, + pipeline_ref_main_pipe=None, + event_log=queue_event_log, + workflow_id="direct", + pipeline_run_id=pipeline_run_id, + ) + + try: + async with Task[PipelexPipeRunStreamingState]( + type=PIPELEX_PIPE_RUN_TASK_TYPE, + state=initial_state, + ) as streaming_task: + forwarder_task = asyncio.create_task( + forward_events_to_task( + event_queue=event_queue, + update_state=streaming_task.update_state, + ), + name=f"pipelex-streaming-forwarder-{pipeline_run_id}", + ) + output: PipelexPipeRunOutput | None = None + try: + output = await run_pipe_via_bridge(input_payload, graph_context=graph_context) + finally: + # Drain ALL pending per-step events BEFORE writing the final + # "completed" state. If we wrote phase="completed" first, the + # forwarder's still-pending pipe_end_success patches would race + # the snapshot and the captured CustomTaskCompleted event would + # read phase="in_progress". + event_queue.put_nowait(SHUTDOWN_SENTINEL) + try: + await asyncio.wait_for(forwarder_task, timeout=get_drain_timeout_seconds()) + except TimeoutError: + forwarder_task.cancel() + # Bridge either returned (output is non-None) or raised inside the + # try block above and we never reach this line. + assert output is not None + await streaming_task.update_state( + { + "phase": "completed", + "pipeline_run_id": output.pipeline_run_id, + "main_stuff_name": output.main_stuff_name, + } + ) + return output + finally: + tracer_manager.close_tracer(pipeline_run_id) + + +async def _run_streaming_without_per_step_events( + input_payload: PipelexPipeRunInput, + initial_state: PipelexPipeRunStreamingState, +) -> PipelexPipeRunOutput: + """TEMPORAL-mode streaming path — Phase 2.0 single-pair semantics, no tracer.""" async with Task[PipelexPipeRunStreamingState]( type=PIPELEX_PIPE_RUN_TASK_TYPE, state=initial_state, diff --git a/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py b/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py new file mode 100644 index 000000000..c3420c4b1 --- /dev/null +++ b/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py @@ -0,0 +1,284 @@ +"""Per-step event forwarding for the streaming activity (Phase 2.1). + +Bridges Pipelex's trace event channel into Mistral's ``Task.update_state``. +``streaming.py`` opens a per-call ``GraphTracerManager`` tracer with a +``QueueEventLog`` injected as the event log, then spawns +``forward_events_to_task`` to drain the queue and translate trace events +into ``Task.update_state(...)`` calls. + +This module is framework-agnostic: it does NOT import ``mistralai.workflows``. +The forwarder takes the bound ``update_state`` coroutine as a callable so the +``Task`` type can stay isolated to ``streaming.py``. +""" + +from __future__ import annotations + +import asyncio +import threading +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Final, Literal + +from typing_extensions import override + +from pipelex import log +from pipelex.graph.graph_config import DataInclusionConfig +from pipelex.tracing.event_log_protocol import EventLogProtocol +from pipelex.tracing.trace_events import ( + BatchAggregateEvent, + BatchItemEvent, + ControllerOutputEvent, + EdgeEvent, + ExecutionDataEvent, + ParallelCombineEvent, + PipeEndErrorEvent, + PipeEndSuccessEvent, + PipeStartEvent, + TraceEvent, + UsageReportEvent, +) + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + +_QUEUE_MAX_SIZE: Final[int] = 256 +_FORWARDER_DRAIN_TIMEOUT_SECONDS: Final[float] = 5.0 + + +class _ShutdownSentinel: + """Module-private sentinel type pushed onto the queue to stop the forwarder.""" + + +SHUTDOWN_SENTINEL: Final[_ShutdownSentinel] = _ShutdownSentinel() + + +_StatePatchKind = Literal["pipe_start", "pipe_end_success", "pipe_end_error"] + + +@dataclass(frozen=True) +class _StatePatch: + """A single Mistral ``Task.update_state`` payload derived from a trace event.""" + + kind: _StatePatchKind + payload: dict[str, Any] + + +class QueueEventLog(EventLogProtocol): + """In-process ``EventLogProtocol`` that pushes events onto an asyncio queue. + + Used by the streaming activity to subscribe to per-step trace events + without persisting them. ``emit`` is called by ``GraphTracer`` from the + pipe-execution context (which may be a worker thread for inference jobs), + so the queue insert is always routed via ``loop.call_soon_threadsafe``. + + Best-effort delivery: when the bounded queue is full, events are dropped + with a one-shot warning. Streaming is observability, not durability. + """ + + def __init__(self, loop: asyncio.AbstractEventLoop, queue: asyncio.Queue[Any]) -> None: + self._loop = loop + self._queue = queue + self._sequence: int = 0 + self._sequence_lock = threading.Lock() + self._writer_id = "mistralai-workflows-streaming" + self._closed = False + self._overflow_warned = False + + @property + @override + def writer_id(self) -> str: + return self._writer_id + + @override + def next_sequence(self) -> int: + with self._sequence_lock: + seq = self._sequence + self._sequence += 1 + return seq + + @override + def emit(self, event: TraceEvent) -> None: + """Push the event onto the queue, routing across threads if needed. + + ``call_soon_threadsafe`` is correct from a worker thread but defers + execution to the next loop iteration; in our hot path the trace + events fire on the same loop as the activity, so we ``put_nowait`` + directly to keep the forwarder fed without an extra trampoline. + """ + if self._closed: + return + try: + running_loop = asyncio.get_running_loop() + except RuntimeError: + running_loop = None + if running_loop is self._loop: + self._enqueue(event) + else: + self._loop.call_soon_threadsafe(self._enqueue, event) + + def _enqueue(self, event: TraceEvent) -> None: + try: + self._queue.put_nowait(event) + except asyncio.QueueFull: + if not self._overflow_warned: + self._overflow_warned = True + log.warning( + f"mistralai_workflows streaming forwarder queue full (maxsize={_QUEUE_MAX_SIZE}); dropping further events for this run.", + ) + + @override + def read_events(self, pipeline_run_id: str) -> list[TraceEvent]: + return [] + + @override + def close(self) -> None: + self._closed = True + + @override + def cleanup(self, pipeline_run_id: str) -> None: + return None + + +def build_streaming_data_inclusion() -> DataInclusionConfig: + """All-flags-off ``DataInclusionConfig`` for the streaming tracer. + + Phase 2.1 only needs pipe metadata (codes, node ids, output spec name); + capturing rendered content / stack traces / registry dumps would just + bloat ``Task.update_state`` payloads with no consumer benefit. + """ + return DataInclusionConfig( + stuff_json_content=False, + stuff_text_content=False, + stuff_html_content=False, + error_stack_traces=False, + pipe_and_concept_registry=False, + ) + + +def _state_patch_for_pipe_start(event: PipeStartEvent, started_steps: int) -> _StatePatch: + return _StatePatch( + kind="pipe_start", + payload={ + "phase": "in_progress", + "current_step_pipe_code": event.pipe_code, + "current_step_node_id": event.node_id, + "last_event_kind": "pipe_start", + "started_steps": started_steps, + }, + ) + + +def _state_patch_for_pipe_end_success(event: PipeEndSuccessEvent, completed_steps: int) -> _StatePatch: + output_stuff_name: str | None = None + if event.output_spec is not None: + output_stuff_name = event.output_spec.name + return _StatePatch( + kind="pipe_end_success", + payload={ + "phase": "in_progress", + "last_event_kind": "pipe_end_success", + "completed_steps": completed_steps, + "last_output_stuff_name": output_stuff_name, + }, + ) + + +def _state_patch_for_pipe_end_error(event: PipeEndErrorEvent) -> _StatePatch: + return _StatePatch( + kind="pipe_end_error", + payload={ + "phase": "in_progress", + "last_event_kind": "pipe_end_error", + "current_step_node_id": event.node_id, + }, + ) + + +def map_trace_event_to_state_patch( + event: TraceEvent, + started_steps: int, + completed_steps: int, +) -> _StatePatch | None: + """Map a trace event to a ``Task.update_state`` patch, or ``None`` to skip. + + Phase 2.1 surfaces only pipe-step boundaries (``PipeStartEvent`` / + ``PipeEndSuccessEvent`` / ``PipeEndErrorEvent``). The other trace event + kinds (edges, batch fan-out, controller outputs, execution metadata, + usage reports) are intentionally suppressed — they fire too frequently + to be useful as Mistral state updates and are already captured by + Pipelex's own reporting / graph infrastructure. + + Mirrors the ``isinstance``-chain pattern used in + ``pipelex.tracing.graphspec_assembler`` for the same union of subclasses. + """ + if isinstance(event, PipeStartEvent): + return _state_patch_for_pipe_start(event=event, started_steps=started_steps) + if isinstance(event, PipeEndSuccessEvent): + return _state_patch_for_pipe_end_success(event=event, completed_steps=completed_steps) + if isinstance(event, PipeEndErrorEvent): + return _state_patch_for_pipe_end_error(event=event) + if isinstance( + event, + ( + EdgeEvent, + ControllerOutputEvent, + BatchItemEvent, + BatchAggregateEvent, + ParallelCombineEvent, + ExecutionDataEvent, + UsageReportEvent, + ), + ): + return None + log.warning(f"Streaming forwarder received unknown trace event type: {type(event).__name__}") + return None + + +async def forward_events_to_task( + event_queue: asyncio.Queue[Any], + update_state: Callable[[dict[str, Any]], Awaitable[None]], +) -> None: + """Drain the event queue, translating each trace event into a state update. + + Runs concurrently with ``run_pipe_via_bridge`` and terminates when the + sentinel is observed. Maintains running counters for ``started_steps`` and + ``completed_steps`` so each emitted patch carries the cumulative count + after the event itself (1-indexed: the first PIPE_START reports + ``started_steps=1``). + + The caller is responsible for posting ``SHUTDOWN_SENTINEL`` and awaiting + this coroutine before letting the parent ``Task`` async-context exit, so + the final per-step ``update_state`` calls are flushed before + ``Task.__aexit__`` closes the task. + """ + started_steps = 0 + completed_steps = 0 + while True: + item = await event_queue.get() + if isinstance(item, _ShutdownSentinel): + return + if not isinstance(item, TraceEvent): + log.warning(f"Streaming forwarder received unexpected queue item type: {type(item).__name__}") + continue + + next_started = started_steps + 1 if isinstance(item, PipeStartEvent) else started_steps + next_completed = completed_steps + 1 if isinstance(item, PipeEndSuccessEvent) else completed_steps + patch = map_trace_event_to_state_patch( + event=item, + started_steps=next_started, + completed_steps=next_completed, + ) + if patch is None: + continue + started_steps = next_started + completed_steps = next_completed + await update_state(patch.payload) + + +def get_drain_timeout_seconds() -> float: + """Expose the forwarder drain timeout for streaming.py to use in wait_for.""" + return _FORWARDER_DRAIN_TIMEOUT_SECONDS + + +def get_queue_max_size() -> int: + """Expose the queue max size for streaming.py to use when constructing the queue.""" + return _QUEUE_MAX_SIZE diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py index 720ab3e54..10c03c2cc 100644 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py @@ -9,7 +9,7 @@ Skipped when ``mistralai-workflows`` is not installed. """ -from typing import Any +from typing import Any, cast import pytest import pytest_asyncio @@ -43,6 +43,7 @@ ) PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" +PIPE_REF_SEQUENCE = "mistralai_workflows_bridge_test.bridge_sequence_pipe" TEST_TASK_QUEUE = "pipelex-mistralai-workflows-streaming-test" @@ -143,3 +144,116 @@ async def test_workflow_emits_custom_task_lifecycle_events( assert completed_payload["phase"] == "completed" assert completed_payload["pipeline_run_id"] == result.pipeline_run_id assert completed_payload["main_stuff_name"] == result.main_stuff_name + + async def test_multistep_pipe_emits_per_step_events( + self, + workflow_env: WorkflowEnvironment, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """A two-step PipeSequence produces one CustomTaskInProgress per pipe boundary.""" + captured_events: list[WorkflowEvent] = [] + mock_events_client = create_capturing_mock_events_client(captured_events) + + payload = PipelexPipeRunInput( + pipe_code=PIPE_REF_SEQUENCE, + inputs={"input_text": "step events"}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + + async with ( + EventContext(events_client=mock_events_client), + create_test_worker_with_events( + workflow_env, + workflows=[PipelexBridgeStreamingTestWorkflow], + activities=[pipelex_run_pipe_streaming], + task_queue=TEST_TASK_QUEUE, + ), + ): + result_dict = await workflow_env.client.execute_workflow( + PipelexBridgeStreamingTestWorkflow.run, + {"payload_dict": payload.model_dump(mode="json")}, + id="pipelex-bridge-streaming-multistep-test-workflow", + task_queue=TEST_TASK_QUEUE, + ) + + result = PipelexPipeRunOutput.model_validate(result_dict) + assert result.is_completed is True + assert result.main_stuff_name is not None + # Both steps must have run in declaration order: upper("step events") wrapped with [STEP2:...] + assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "[STEP2:STEP EVENTS]" + + custom_task_events = [ + event + for event in captured_events + if isinstance(event, (CustomTaskStarted, CustomTaskInProgress, CustomTaskCompleted)) + and event.attributes.custom_task_type == PIPELEX_PIPE_RUN_TASK_TYPE + ] + started_events = [event for event in custom_task_events if isinstance(event, CustomTaskStarted)] + in_progress_events = [event for event in custom_task_events if isinstance(event, CustomTaskInProgress)] + completed_events = [event for event in custom_task_events if isinstance(event, CustomTaskCompleted)] + + assert len(started_events) == 1, f"expected 1 CustomTaskStarted, got {len(started_events)}" + assert len(completed_events) == 1, f"expected 1 CustomTaskCompleted, got {len(completed_events)}" + + # CustomTaskInProgress carries a JSONPatchPayload — value is a list of JSON Patch operations + # (one per field that *changed* between previous and new state). Flatten each event's patches + # into a {field: value} dict. + # + # Important: a field only appears in the patch when its value actually changed. If two + # consecutive update_state calls write the same value to a field (e.g. last_event_kind back + # to "pipe_start" without a "pipe_end_success" in between), that field is absent from the + # second patch. We therefore key on /started_steps (strictly monotonic on every pipe_start) + # and /completed_steps (strictly monotonic on every pipe_end_success). + patches_per_event = [_patches_to_changes(event) for event in in_progress_events] + + pipe_start_changes = [changes for changes in patches_per_event if "started_steps" in changes] + pipe_end_success_changes = [changes for changes in patches_per_event if "completed_steps" in changes] + assert len(pipe_start_changes) >= 3, f"expected >=3 pipe_start in_progress events, got {len(pipe_start_changes)}" + assert len(pipe_end_success_changes) >= 3, f"expected >=3 pipe_end_success in_progress events, got {len(pipe_end_success_changes)}" + + # Order: outer PipeSequence first, then step_one, then step_two. /current_step_pipe_code + # changes on every pipe_start (each pipe has a distinct code) so it always appears in the patch. + step_codes_in_order = [changes["current_step_pipe_code"] for changes in pipe_start_changes] + assert step_codes_in_order[0].endswith("bridge_sequence_pipe") + assert step_codes_in_order[1].endswith("bridge_seq_step_one") + assert step_codes_in_order[2].endswith("bridge_seq_step_two") + + # started_steps counter is monotonic 1, 2, 3 across the first three pipe_start events. + started_steps_seq = [changes["started_steps"] for changes in pipe_start_changes[:3]] + assert started_steps_seq == [1, 2, 3] + + # completed_steps reaches at least 3 by the end. + max_completed = max(changes["completed_steps"] for changes in pipe_end_success_changes) + assert max_completed >= 3 + + # Phase 2.0 fields still surfaced on the final completed event (a JSONPayload — full state snapshot). + completed_payload = completed_events[0].attributes.payload.value + assert completed_payload["phase"] == "completed" + assert completed_payload["pipeline_run_id"] == result.pipeline_run_id + assert completed_payload["main_stuff_name"] == result.main_stuff_name + + +def _patches_to_changes(event: CustomTaskInProgress) -> dict[str, Any]: + """Flatten a CustomTaskInProgress JSON Patch list into a {field: value} dict. + + Each ``update_state`` call produces a single ``CustomTaskInProgress`` with + a list of root-level "add"/"replace" patches (paths look like ``/field``). + Returns a dict of just the fields that changed in this event. + """ + changes: dict[str, Any] = {} + payload_value: Any = event.attributes.payload.value + if not isinstance(payload_value, list): + return changes + raw_patches = cast("list[Any]", payload_value) + for raw_patch in raw_patches: + if isinstance(raw_patch, dict): + patch_dict = cast("dict[str, Any]", raw_patch) + else: + patch_dict = cast("dict[str, Any]", raw_patch.model_dump()) + op = patch_dict.get("op") + path = patch_dict.get("path", "") + if op in {"add", "replace"} and isinstance(path, str) and path.startswith("/"): + field = path[1:] + if field: + changes[field] = patch_dict.get("value") + return changes diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds index 20fa07114..9cc1f048c 100644 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds +++ b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds @@ -30,3 +30,27 @@ output = "EchoEnvelope" [pipe.bridge_envelope_pipe.construct] text = { from = "input_text.text" } origin = "mistralai_workflows_bridge" + +[pipe.bridge_seq_step_one] +type = "PipeCompose" +description = "First step of bridge_sequence_pipe — uppercases the input text" +inputs = { input_text = "Text" } +output = "Text" +template = "{{ input_text.text | upper }}" + +[pipe.bridge_seq_step_two] +type = "PipeCompose" +description = "Second step of bridge_sequence_pipe — wraps the result with markers" +inputs = { uppercased = "Text" } +output = "Text" +template = "[STEP2:{{ uppercased.text }}]" + +[pipe.bridge_sequence_pipe] +type = "PipeSequence" +description = "Two-step sequence used by Phase 2.1 streaming tests to assert per-step events" +inputs = { input_text = "Text" } +output = "Text" +steps = [ + { pipe = "bridge_seq_step_one", result = "uppercased" }, + { pipe = "bridge_seq_step_two", result = "final_text" }, +] From c7b00dcce99460495280b42e5da12748676f1391 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 09:21:43 +0200 Subject: [PATCH 07/16] Drop assert-based narrowing in streaming bridge Remove the `output: PipelexPipeRunOutput | None = None` initializer and the trailing `assert output is not None` in `_run_streaming_with_per_step_events`. The narrowing was assert-based (stripped under `python -O`) and only existed to satisfy the type checker after a `try`/`finally` cleanup block. With the pre-init dropped, pyright narrows `output` from the `try` body alone, since the post-`try/finally` code is only reachable on success. Extend the drain comment to call out that the cleanup serves both the happy path (snapshot ordering) and the failure path (publishing pending in-progress events before `Task.__aexit__` emits `CustomTaskFailed`). --- pipelex/plugins/mistralai_workflows/streaming.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pipelex/plugins/mistralai_workflows/streaming.py b/pipelex/plugins/mistralai_workflows/streaming.py index d1ee5f1d7..299150c0a 100644 --- a/pipelex/plugins/mistralai_workflows/streaming.py +++ b/pipelex/plugins/mistralai_workflows/streaming.py @@ -179,7 +179,6 @@ async def _run_streaming_with_per_step_events( ), name=f"pipelex-streaming-forwarder-{pipeline_run_id}", ) - output: PipelexPipeRunOutput | None = None try: output = await run_pipe_via_bridge(input_payload, graph_context=graph_context) finally: @@ -187,15 +186,14 @@ async def _run_streaming_with_per_step_events( # "completed" state. If we wrote phase="completed" first, the # forwarder's still-pending pipe_end_success patches would race # the snapshot and the captured CustomTaskCompleted event would - # read phase="in_progress". + # read phase="in_progress". On the failure path, the drain + # also lets pending in-progress events publish before the + # surrounding ``async with Task`` emits CustomTaskFailed. event_queue.put_nowait(SHUTDOWN_SENTINEL) try: await asyncio.wait_for(forwarder_task, timeout=get_drain_timeout_seconds()) except TimeoutError: forwarder_task.cancel() - # Bridge either returned (output is non-None) or raised inside the - # try block above and we never reach this line. - assert output is not None await streaming_task.update_state( { "phase": "completed", From 4a5002e081fff7d955fe4fd004482cd8dcb8e860 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 09:26:50 +0200 Subject: [PATCH 08/16] Update dependencies in pyproject.toml and uv.lock - Updated the `instructor` dependency to a new commit hash for improved functionality. --- pyproject.toml | 2 +- uv.lock | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 42118e4ec..375192dea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -456,7 +456,7 @@ required-version = ">=0.7.2" [tool.uv.sources] # Temporary: pin to fork that adds mistralai 2.x support. Revert to PyPI release once # https://github.com/567-labs/instructor/pull/2298 is merged and published. -instructor = { git = "https://github.com/Ian321/instructor.git", rev = "0efd9c09b05ef561defff3a8b86fe86e5e61214c" } +instructor = { git = "https://github.com/Ian321/instructor.git", rev = "4ea22d2396ca35514929f27faed866115e8b3583" } [tool.hatch.build.targets.wheel] packages = ["pipelex"] diff --git a/uv.lock b/uv.lock index daab350da..082b74841 100644 --- a/uv.lock +++ b/uv.lock @@ -745,7 +745,7 @@ name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, @@ -776,37 +776,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1505,7 +1505,7 @@ wheels = [ [[package]] name = "instructor" version = "1.15.1" -source = { git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c#0efd9c09b05ef561defff3a8b86fe86e5e61214c" } +source = { git = "https://github.com/Ian321/instructor.git?rev=4ea22d2396ca35514929f27faed866115e8b3583#4ea22d2396ca35514929f27faed866115e8b3583" } dependencies = [ { name = "aiohttp" }, { name = "docstring-parser" }, @@ -2602,7 +2602,7 @@ name = "nvidia-cudnn-cu13" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, @@ -2614,7 +2614,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -2644,9 +2644,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -2658,7 +2658,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -3266,8 +3266,8 @@ requires-dist = [ { name = "google-genai", marker = "extra == 'google-genai'" }, { name = "httpx", specifier = ">=0.23.0,<1.0.0" }, { name = "huggingface-hub", marker = "extra == 'huggingface'", specifier = ">=0.23,<1.0.0" }, - { name = "instructor", git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c" }, - { name = "instructor", extras = ["google-genai"], marker = "extra == 'google-genai'", git = "https://github.com/Ian321/instructor.git?rev=0efd9c09b05ef561defff3a8b86fe86e5e61214c" }, + { name = "instructor", git = "https://github.com/Ian321/instructor.git?rev=4ea22d2396ca35514929f27faed866115e8b3583" }, + { name = "instructor", extras = ["google-genai"], marker = "extra == 'google-genai'", git = "https://github.com/Ian321/instructor.git?rev=4ea22d2396ca35514929f27faed866115e8b3583" }, { name = "jinja2", specifier = ">=3.1.4" }, { name = "json2html", specifier = ">=1.3.0" }, { name = "kajson", specifier = "==0.5.0" }, From e33b468c6e4966f702be420597d9fccd985bbbcd Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 09:35:13 +0200 Subject: [PATCH 09/16] Move Mistral Workflows planning docs into wip/ Relocate the in-tree TODOS.md to wip/mistral-workflows-sub-module.md and add wip/mistral-workflows-plugin-extract.md outlining the extraction of the Mistral Workflows integration into its own pip-installable plugin package scaffolded from pipelex-starter-python. --- wip/mistral-workflows-plugin-extract.md | 215 ++++++++++++++++++ .../mistral-workflows-sub-module.md | 0 2 files changed, 215 insertions(+) create mode 100644 wip/mistral-workflows-plugin-extract.md rename TODOS.md => wip/mistral-workflows-sub-module.md (100%) diff --git a/wip/mistral-workflows-plugin-extract.md b/wip/mistral-workflows-plugin-extract.md new file mode 100644 index 000000000..fed612968 --- /dev/null +++ b/wip/mistral-workflows-plugin-extract.md @@ -0,0 +1,215 @@ +# Mistral Workflows ↔ Pipelex — Extract as a Real Mistral Plugin + +Self-contained plan. Sibling document `mistral-workflows-sub-module.md` (the +former `TODOS.md`) records what was built in-tree under +`pipelex/plugins/mistralai_workflows/` during Phases 1.x–2.1. That work is the +*input* to this project, not background to redo. + +## 1. Why + +What we shipped is the **functional equivalent** of a Mistral Workflows plugin +but packaged as an optional extra of `pipelex` (`pipelex[mistralai-workflows]`, +imported from `pipelex.plugins.mistralai_workflows.*`). Mistral defines a +plugin as a **standalone, pip-installable package** that depends on +`mistralai-workflows>=2.0.0` and exposes activities / workflows / dependencies +under its own top-level package (the `mistralai.workflows.plugins.*` +namespace is reserved for Mistral-supported packages). + +To match that contract we need to: + +1. Stop shipping Mistral-specific code inside the `pipelex` distribution. +2. Ship a separate distribution (PyPI: `pipelex-mistralai-workflows`, + Python package: `pipelex_mistralai_workflows`) that `pip install`s + `pipelex` and `mistralai-workflows>=3.3.0` and re-exports the same + activities, with a Mistral-style "component" / dependency wrapper. +3. Keep the framework-agnostic embedding core (`bridge.py`, + `execution_mode.py`, `bootstrap.py`, `exceptions.py`) reachable from + *any* host, not just Mistral — so other durable runtimes (raw Temporal, + future plugins) can reuse it. + +## 2. Goals & non-goals + +**Goals** + +- Pure-Python `pipelex` distribution: no `mistralai-workflows` extra, no + `pipelex/plugins/mistralai_workflows/` directory. +- New repo `pipelex-mistralai-workflows` scaffolded from + `pipelex-starter-python` and adapted for a library (not an app), with + identical lint/type/test toolchain to the rest of the workspace. +- Public surface preserved: a user who today writes + `from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe` + has a clear, mechanical migration to the new import path. +- Mistral-idiomatic ergonomics: a `pipelex_dependency` (or equivalent + component) so workers wire Pipelex the same way they wire + `mistralai_chat_complete` today. +- CI on the new repo runs the integration test layers that need the + optional dep (currently layer-2 / layer-3 in `mistral-workflows-sub-module.md` §3). +- Docs split cleanly: embedding-core docs stay in pipelex; Mistral-specific + recipes move to the new repo's docs. + +**Non-goals** + +- Re-doing Phase 1.x / Phase 2.x design. The behavior, boundary types, + execution modes, streaming semantics, and gotchas are all locked in — + see `mistral-workflows-sub-module.md` §2 and §4. Treat them as spec. +- Goal 1 from the original doc (porting Pipelex orchestration to run *on* + Mistral Workflows). Still out of scope. +- Backwards-compatibility shims in `pipelex` for the old import path. Per + project rule (CLAUDE.md "No backward compatibility"), we just change it + and note the migration in the changelog. + +## 3. Decision points to resolve before coding + +A follow-up agent should not start until these are answered. Each has a +proposed default; flag any that need a human call. + +1. **Where does the framework-agnostic core live?** + - Option A *(recommended default)*: keep `bridge.py`, + `execution_mode.py`, `bootstrap.py`, `exceptions.py` inside `pipelex`, + promoted out of `plugins/mistralai_workflows/` into something like + `pipelex/embedding/` (name TBD). The new plugin pkg imports from there. + Pros: any host (raw Temporal, future plugins) can reuse it; smaller + surface to maintain in the new repo. + - Option B: move *everything* into `pipelex-mistralai-workflows`. Pros: + `pipelex` stays leaner. Cons: any future host has to either depend on + the Mistral plugin pkg (wrong) or re-implement. + - This decision drives the rest of the file moves. + +2. **Package + distribution names.** + - PyPI: `pipelex-mistralai-workflows` (proposed). Confirm naming aligns + with other Pipelex packages on PyPI. + - Top-level Python package: `pipelex_mistralai_workflows`. The + `mistralai.workflows.plugins.*` namespace is reserved, so we cannot + squat there. + - GitHub repo: `pipelex-mistralai-workflows` under the existing org, + side-by-side with the other repos listed in the workspace `CLAUDE.md`. + +3. **Versioning & release cadence.** + - Pin a minimum `pipelex` version per release. Decide whether to track + `pipelex` major versions 1:1 or use independent SemVer. + - Decide whether the new repo follows the same release skill / version + conventions as `pipelex` (CHANGELOG.md format, `release/vX.Y.Z` + branches, etc.). Default: yes, identical. + +4. **Mistral component wrapper shape.** + - Mistral's existing plugins ship a "dependency" (e.g. + `mistralai_chat_complete`) that workers register. We should ship at + least one — likely a wrapper around `ensure_pipelex_booted()` plus a + `LibraryCrate` snapshot. Final shape needs a quick read of how + `mistralai.workflows.plugins.mistralai` exposes its dependency before + committing. + +5. **Cookbook example.** + - The deferred Phase 1.3 cookbook entry (`pipelex-cookbook/examples/c_advanced/mistral-workflows/`) + should land *after* the new package is on PyPI, importing from the new + path. Coordinate timing. + +## 4. Workstreams + +Three streams, parallelizable once §3 is resolved. Each has its own +follow-up agent / PR. + +### Stream A — Refactor inside `pipelex` + +In this repo (`_mistral/`, eventually merged back). High-level only; the +follow-up agent figures out the file moves once §3.1 is decided. + +- Lift the framework-agnostic core to its new home (Option A) or remove it + entirely (Option B). +- Delete the Mistral-specific modules (`activities.py`, `streaming.py`, + `streaming_event_forwarder.py`) and their unit + integration tests. +- Drop the `[mistralai-workflows]` extra from `pyproject.toml`. Drop the + `[[tool.mypy.overrides]]` block that exists only because Mistral's + source uses PEP 695 syntax (re-add it in the new repo). +- Move docs: `docs/under-the-hood/mistralai-workflows-{plugin,recipes}.md` + go to the new repo's docs site. Leave a stub in pipelex docs that links + out. +- CHANGELOG entry under `[Unreleased]` describing the move + migration. +- Verify: `make agent-check` and `make agent-test` green; `git grep + mistralai_workflows` returns nothing in `pipelex/` after the move. + +### Stream B — Scaffold `pipelex-mistralai-workflows` from `pipelex-starter-python` + +New repo, side-by-side with the other workspace repos. + +- Copy `pipelex-starter-python/` as the starting point. Rename the package + dir, rewrite `pyproject.toml` (`name`, `description`, dependencies, + package list, classifiers). +- Convert from "app starter" to "library": + - Drop the `my_project/hello_world.{mthds,py}` example. + - Add a real `LICENSE` (MIT, matching pipelex). + - Replace the README with one that explains: install, register the + activity on a worker, call it from a workflow, link to recipes. + - Wire `py.typed` (already present in starter — keep). +- `pyproject.toml` deltas vs starter: + - `name = "pipelex-mistralai-workflows"`. + - `dependencies = ["pipelex>=X.Y", "mistralai-workflows>=3.3.0"]` (no + `[mistralai,anthropic,...]` extras — this is a library, not an app). + - Re-add the `[[tool.mypy.overrides]]` for `mistralai.workflows.*` + (PEP 695 source) — copy from pipelex `pyproject.toml`. + - Pytest markers: copy the relevant subset; drop `inference`/`llm`/etc. + if the test suite only does layer-2 / layer-3 worker tests. +- Add a Makefile mirroring the `agent-check` / `agent-test` / `cleanderived` + targets used elsewhere in the workspace, so CLAUDE.md instructions in + this repo and the new repo overlap. +- Add `CLAUDE.md` for the new repo. Short — point at workspace `CLAUDE.md` + and call out: must not depend on internal `pipelex` paths, only the + promoted public embedding core. +- GitHub Actions: copy the matrix from `pipelex` if there is one, or wire + a minimal `uv sync && make agent-check && make agent-test` workflow. + +### Stream C — Move the plugin code into the new repo + +After A and B land (or in a coordinated PR pair). + +- Move `activities.py`, `streaming.py`, `streaming_event_forwarder.py` + into `pipelex_mistralai_workflows/`. Rewrite imports to point at the + promoted embedding core in `pipelex`. +- Move the integration tests (`test_activities_direct.py`, + `test_activities_offloaded.py`, `test_activities_streaming.py`, + `test_bridge_temporal_blocking.py`, `test_bridge_temporal_fire_and_forget.py`) + and their fixtures (`conftest.py`, `test_data/bridge_test.mthds`, + `test_data/bridge_funcs.py`). +- Layer-1 framework-agnostic tests (`test_bridge_direct.py`, the unit tests + under `tests/unit/pipelex/plugins/mistralai_workflows/`) follow the + embedding core — they stay in `pipelex` if we picked Option A, move if + Option B. +- Add the Mistral component / dependency wrapper from §3.4. +- First release to PyPI as `0.1.0`. Tag, write release notes, link from + the pipelex CHANGELOG migration entry. + +## 5. Migration story for users + +Single-paragraph note in pipelex CHANGELOG and in the new repo's README: + +> Mistral Workflows integration moved from +> `pipelex[mistralai-workflows]` / `pipelex.plugins.mistralai_workflows.*` +> into a dedicated package: `pip install pipelex-mistralai-workflows` and +> import from `pipelex_mistralai_workflows.*`. No API changes; the +> activities, boundary types, and execution modes are identical. + +Per project rule, no compat shim. The pipelex release that drops the +extra and the first `pipelex-mistralai-workflows` release ship together. + +## 6. Open risks (track but don't block) + +- **Version coupling.** The plugin pkg depends on internal-but-public + embedding APIs of `pipelex`. Decide on a stable surface (probably what + `bridge.py` already exposes) and document it as such, or breakage will + cascade on every pipelex release. +- **Test parity in CI.** Today layer-2 / layer-3 tests run on every PR in + pipelex CI because `uv sync --all-extras` pulls the optional dep. Once + extracted, those tests only run in the new repo. Make sure both repos' + CI matrices are healthy before flipping the switch. +- **OffloadableField import path drift.** Already noted in + `mistral-workflows-sub-module.md` §6. Carries over verbatim. + +## 7. Resuming a session + +1. Read `mistral-workflows-sub-module.md` §2, §4, §5 — these are the + binding design decisions and gotchas you must respect. +2. Read this file end-to-end. +3. Resolve §3 with the user before writing code. Especially §3.1 (where + the framework-agnostic core lives) — every other choice depends on it. +4. Pick a stream from §4. Streams A and B can run in parallel; Stream C + waits on both. diff --git a/TODOS.md b/wip/mistral-workflows-sub-module.md similarity index 100% rename from TODOS.md rename to wip/mistral-workflows-sub-module.md From 3269d93c63364a32c829db2ba758041ce73dd954 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 10:49:51 +0200 Subject: [PATCH 10/16] Add execution plan for Mistral Workflows plugin extraction --- TODOS.md | 665 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 665 insertions(+) create mode 100644 TODOS.md diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 000000000..54d4a1a5b --- /dev/null +++ b/TODOS.md @@ -0,0 +1,665 @@ +# Mistral Workflows ↔ Pipelex — Plugin Extraction TODOs + +Concrete execution plan for the migration described in +`wip/mistral-workflows-plugin-extract.md`. Read that file plus the +binding design decisions in `wip/mistral-workflows-sub-module.md` §2 and §4 +before starting any task here. + +**Two repos involved** + +- `_workflows/` — git worktree of `pipelex` on branch + `feature/Adapt-mistral-workflows`. Holds the code being extracted. +- `../pipelex-mistralai-workflows/` — already scaffolded from + `pipelex-starter-python` (currently looks like the starter app, needs to be + converted to a library). + +The new repo's package directory is `pipelex_mistralai_workflows/` and the +PyPI name is `pipelex-mistralai-workflows`. The scaffold version sits at +`0.8.0` (inherited from the starter); we will reset to `0.1.0` as the first +real release of this project. + +--- + +## 0. Pre-decisions (lock these before writing code) + +Defaults below are the recommended path. Override only if there's a concrete +reason; otherwise proceed. + +- [ ] **0.1 — Where the framework-agnostic core lives.** Default: `pipelex/embedding/`. + Free package name (verified — no clash with existing modules; the + "embedding" hits in pipelex are unrelated HTML / jinja2 string usages). + The name communicates *embedding the Pipelex runtime into another + host runtime*. If the vector-embedding overlap feels confusing later, + `pipelex.runtime_bridge` is the fallback. +- [ ] **0.2 — Mistral-specific bits stay in the new repo, agnostic bits move + to `pipelex.embedding`.** Concrete split: + - **Move to `pipelex/embedding/`:** `bridge.py`, `execution_mode.py`, + `bootstrap.py::ensure_pipelex_booted`, the agnostic exceptions + (`PipelexBridgeRuntimeError`, `MissingPipelexTemporalExtraError`). + - **Move to `pipelex_mistralai_workflows/`:** `activities.py`, + `streaming.py`, `streaming_event_forwarder.py`, + `bootstrap.py::get_pipelex_dependency` (Mistral-shaped — references + `mistralai.workflows.Depends`). + - **Delete entirely:** `MistralWorkflowsPluginError`, + `MistralWorkflowsNotInstalledError`. Once `mistralai-workflows>=3.3.0` + is a hard dep of the new repo, the optional-dep guards in + `activities.py` / `streaming.py` are obsolete and the import-fail + exception goes with them. +- [ ] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** Currently + `0.8.0` (starter inheritance) — that version space is wrong for a + brand-new project. First release ships as `v0.1.0`. +- [ ] **0.4 — Pin `pipelex>=NEXT` in the new repo.** `NEXT` is whatever + pipelex version lands the `pipelex.embedding` package. Bump the + minimum on every pipelex release that touches the embedding surface. + Independent SemVer for the plugin pkg. +- [ ] **0.5 — Mistral component / dependency wrapper shape.** Open: read + `mistralai.workflows.plugins.mistralai` (the reference plugin) before + committing to a shape. Stream C task C4 below holds the placeholder. +- [ ] **0.6 — Cookbook entry timing.** Defer + `pipelex-cookbook/examples/c_advanced/mistral-workflows/` until after + `pipelex-mistralai-workflows==0.1.0` is on PyPI (Stream D). + +--- + +## Stream A — Refactor inside `pipelex` (this worktree) + +Goal: end state where `git grep mistralai_workflows` and `git grep +mistralai-workflows` both return zero hits inside `pipelex/`, and the +framework-agnostic core lives at `pipelex.embedding.*`. + +### A1. Create the new package + +- [ ] Create `pipelex/embedding/` with an empty `__init__.py` (no + re-exports — Pipelex rule). + +### A2. Move `bridge.py` + +- [ ] Move `pipelex/plugins/mistralai_workflows/bridge.py` → + `pipelex/embedding/bridge.py`. +- [ ] Rewrite imports inside `bridge.py`: + - `from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted` + → `from pipelex.embedding.bootstrap import ensure_pipelex_booted` + - `from pipelex.plugins.mistralai_workflows.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` + → `from pipelex.embedding.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` + - `from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode` + → `from pipelex.embedding.execution_mode import PipelexExecutionMode` +- [ ] Rename the per-call library id prefix on line 222: + `f"mistralai_workflows_{uuid4().hex[:8]}"` → + `f"embedding_{uuid4().hex[:8]}"`. +- [ ] Update the install hint in `_require_pipelex_temporal_extra` (line 336): + `"pip install 'pipelex[temporal,mistralai-workflows]'"` → + `"pip install 'pipelex[temporal]'"`. +- [ ] Update the module docstring: drop "of the mistralai_workflows plugin", + reframe as "framework-agnostic Pipelex embedding surface for host + runtimes (Mistral Workflows, raw Temporal, future plugins)". + +### A3. Move `execution_mode.py` + +- [ ] Move `pipelex/plugins/mistralai_workflows/execution_mode.py` → + `pipelex/embedding/execution_mode.py`. No import changes inside the + file. + +### A4. Move `bootstrap.py` (split — keep agnostic, drop Mistral-shaped) + +- [ ] Move `pipelex/plugins/mistralai_workflows/bootstrap.py` → + `pipelex/embedding/bootstrap.py`. +- [ ] Keep `ensure_pipelex_booted(...)` verbatim. Update the module + docstring: drop "for use inside Mistral Workflows activities", reframe + as "for use inside any host runtime that embeds Pipelex". +- [ ] **Delete** `get_pipelex_dependency()` from `pipelex/embedding/bootstrap.py` + — it returns a callable explicitly shaped for `mistralai.workflows.Depends` + and belongs in the new repo. Its replacement lives in + `pipelex_mistralai_workflows/dependency.py` (Stream C, task C4). + +### A5. Split `exceptions.py` + +- [ ] Create `pipelex/embedding/exceptions.py` with: + - `PipelexEmbeddingError(PipelexError)` — new base (replaces + `MistralWorkflowsPluginError`). + - `MissingPipelexTemporalExtraError(PipelexEmbeddingError)`. + - `PipelexBridgeRuntimeError(PipelexEmbeddingError)`. +- [ ] **Do NOT** carry `MistralWorkflowsNotInstalledError` over — it goes + away entirely (the new repo has `mistralai-workflows>=3.3.0` as a + hard dep, so the optional-dep guard pattern is obsolete). + +### A6. Delete the old plugin directory + +- [ ] After A2–A5 are complete and tests still pass, delete the entire + directory `pipelex/plugins/mistralai_workflows/`. This includes: + - `__init__.py` + - `bridge.py` (moved in A2) + - `bootstrap.py` (moved in A4) + - `exceptions.py` (split in A5) + - `execution_mode.py` (moved in A3) + - `activities.py` (deleted; lives in new repo per Stream C) + - `streaming.py` (deleted; lives in new repo per Stream C) + - `streaming_event_forwarder.py` (deleted; lives in new repo per Stream C) + +### A7. Update `pyproject.toml` + +- [ ] Remove the `mistralai-workflows = ["mistralai-workflows>=3.3.0"]` + entry from `[project.optional-dependencies]` (currently line 88). +- [ ] Remove the entire `[[tool.mypy.overrides]]` block for + `mistralai.workflows.*` / `mistralai.workflows` (currently lines + 154–164). Pipelex no longer imports anything from that namespace. + +### A8. Move/delete tests + +Layer-1 (framework-agnostic) tests follow the embedding core into pipelex. +Layer-2 / layer-3 tests (which actually instantiate Mistral +`WorkflowEnvironment` / activities) go to the new repo via Stream C. + +- [ ] **Move** `tests/unit/pipelex/plugins/mistralai_workflows/` → + `tests/unit/pipelex/embedding/`: + - `test_input_models.py` + - `test_execution_mode.py` + - `test_validation.py` + - `test_dispatch.py` + - In each, rewrite `pipelex.plugins.mistralai_workflows.*` imports → + `pipelex.embedding.*`. +- [ ] **Move** the layer-1 integration test: + `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` + → `tests/integration/pipelex/embedding/test_bridge_direct.py`. + Rewrite imports. +- [ ] **Move the conftest + test_data with it.** They are needed by the + layer-1 test that stays in pipelex AND will be copied to the new repo + (Stream C, C6) for the layer-2 / layer-3 tests: + - `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` + → `tests/integration/pipelex/embedding/conftest.py`. Update the import + path inside (`from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs` + → `from tests.integration.pipelex.embedding.test_data.bridge_funcs`). + - `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` + → `tests/integration/pipelex/embedding/test_data/` (`bridge_test.mthds` + + `bridge_funcs.py`). + - Update the `domain` in `bridge_test.mthds` if the prefix + `mistralai_workflows_bridge_test` reads weirdly post-move; suggest + keeping the existing domain string for the move PR to minimize churn, + rename in a follow-up if needed. Tests reference the literal pipe + refs so any rename must be coordinated. +- [ ] **Delete** the layer-2 / layer-3 integration tests (they move to the + new repo via Stream C): + - `test_activities_direct.py` + - `test_activities_offloaded.py` + - `test_activities_streaming.py` + - `test_bridge_temporal_blocking.py` + - `test_bridge_temporal_fire_and_forget.py` +- [ ] Delete the now-empty + `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs. + +### A9. Move docs + +- [ ] **Delete** `docs/under-the-hood/mistralai-workflows-plugin.md` and + `docs/under-the-hood/mistralai-workflows-recipes.md`. Their content + moves to the new repo's docs (Stream B, B3 README + future docs site). +- [ ] **Update `mkdocs.yml`** — remove four lines: + - line 310: `- under-the-hood/mistralai-workflows-plugin.md: "Mistral Workflows Plugin"` + - line 311: `- under-the-hood/mistralai-workflows-recipes.md: "Mistral Workflows Recipes"` + - line 500: `- Mistral Workflows Plugin: under-the-hood/mistralai-workflows-plugin.md` + - line 501: `- Mistral Workflows Recipes: under-the-hood/mistralai-workflows-recipes.md` +- [ ] **Optional stub.** If we want a discoverable redirect, add a single + short page `docs/under-the-hood/mistralai-workflows.md` containing a + one-paragraph "moved to a separate package" notice with a link to + the new repo. Re-wire `mkdocs.yml` to reference it. Default: skip + the stub — the CHANGELOG migration entry (A10) covers discovery. + +### A10. Update `CHANGELOG.md` + +The current `[Unreleased]` section has three entries documenting the plugin +landing. Replace with the migration story. + +- [ ] Remove the three plugin-specific bullets from `[Unreleased]` (the + `pipelex.plugins.mistralai_workflows` activity, the streaming + variant, the per-step streaming additions). They will live in the + new repo's CHANGELOG (Stream B, B5). +- [ ] Add a new `[Unreleased]` bullet: + + > **Mistral Workflows integration extracted into a dedicated package.** + > The optional `pipelex[mistralai-workflows]` extra and the + > `pipelex.plugins.mistralai_workflows.*` modules have been removed + > from `pipelex`. Install the new package instead: + > `pip install pipelex-mistralai-workflows`, and import from + > `pipelex_mistralai_workflows.*`. The framework-agnostic embedding + > core (boundary types, `run_pipe_via_bridge`, `PipelexExecutionMode`, + > `ensure_pipelex_booted`) has been promoted from + > `pipelex.plugins.mistralai_workflows.*` to `pipelex.embedding.*` so + > any host runtime — not just Mistral Workflows — can embed Pipelex. + > No behavior changes; activities, boundary types, and execution + > modes are identical. + + Per project rule (CLAUDE.md "No backward compatibility"), no compat + shim. The pipelex release that drops the extra ships together with + `pipelex-mistralai-workflows==0.1.0`. + +### A11. Verify + +- [ ] `make agent-check` clean. +- [ ] `make agent-test` green. +- [ ] `git grep mistralai_workflows pipelex/ tests/ pyproject.toml` returns no hits. +- [ ] `git grep mistralai-workflows pipelex/ tests/ pyproject.toml` returns + only the migration paragraph in `CHANGELOG.md` and the install hint + in `_require_pipelex_temporal_extra` (now removed per A2 — verify). +- [ ] `git grep "pipelex.embedding" pipelex/ tests/` finds the new package + paths. + +### A12. (Out-of-scope reminder) Verify "make agent-check passes without optional dep" + +The outstanding box from `mistral-workflows-sub-module.md` §Outstanding +("`make agent-check` passes with `mistralai-workflows` NOT installed") +becomes trivially true once A6 + A7 are done — `pipelex` no longer imports +`mistralai.workflows` anywhere. No separate verification step needed. + +--- + +## Stream B — Adapt the `pipelex-mistralai-workflows` scaffold + +Currently the repo at `../pipelex-mistralai-workflows/` is the +`pipelex-starter-python` scaffold with a `hello_world` example. Convert +to a library distribution. + +### B1. Strip starter content + +- [ ] Delete `pipelex_mistralai_workflows/hello_world.py`. +- [ ] Delete `pipelex_mistralai_workflows/hello_world.mthds`. +- [ ] Keep `pipelex_mistralai_workflows/__init__.py` (empty) and + `pipelex_mistralai_workflows/py.typed`. +- [ ] Delete `tests/test_pipelines/` (starter artifact — no test pipelines + yet) and `tests/e2e/test_pipelex_mistralai_workflows.py` (starter + smoke test that imports `hello_world`). Layer-1+ tests come from + Stream C. + +### B2. Rewrite `pyproject.toml` + +- [ ] `version = "0.1.0"` (currently `0.8.0`). +- [ ] `description = "Mistral Workflows plugin for Pipelex — invoke Pipelex pipes from inside Mistral Workflows activities."` + (currently a placeholder). +- [ ] Uncomment `authors` and set to + `[{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }]` (matching + pipelex). +- [ ] Update `[project.urls]`: + - `Homepage = "https://pipelex.com"` + - `Repository = "https://github.com/Pipelex/pipelex-mistralai-workflows"` + - `Documentation = "https://docs.pipelex.com/"` +- [ ] Replace `dependencies = ["pipelex[mistralai,anthropic,...]>=0.26.4"]` + with the slim library shape: + + ```toml + dependencies = [ + "pipelex>=NEXT", # NEXT = the version that ships pipelex.embedding + "mistralai-workflows>=3.3.0", + ] + ``` + + No inference / cloud extras. This is a library, not an app. +- [ ] Add an optional extra for the Temporal layer-3 tests: + + ```toml + [project.optional-dependencies] + temporal = ["pipelex[temporal]>=NEXT"] + ``` +- [ ] Add the PEP 695 mypy override that pipelex used to carry — Mistral's + source still uses PEP 695 type parameters mypy rejects under the + project's `python_version`. Copy the block (lines 154–164 in + pipelex's old `pyproject.toml` — moved here in Stream A task A7): + + ```toml + [[tool.mypy.overrides]] + follow_imports = "skip" + ignore_errors = true + module = ["mistralai.workflows.*", "mistralai.workflows"] + ``` +- [ ] Add `pytest-asyncio>=0.24.0`, `pytest-mock>=3.14.0` to the `dev` + extra. +- [ ] **Pytest markers** — keep only the markers the test suite actually + uses. Drop `inference` / `llm` / `img_gen` / `extract` / `pipelex_api` + (the layer-2/3 tests don't call inference). Keep: + - `gha_disabled` + - `dry_runnable` + - `temporal: tests that require a Temporal server` (mirror pipelex's) +- [ ] Reconsider `requires-python`. The scaffold is `>=3.12,<3.15` (because + `mistralai-workflows` requires 3.12+). pipelex itself targets 3.10+. + Keep `>=3.12,<3.15` here — Mistral Workflows is the binding floor. + Confirm by checking `mistralai-workflows` PyPI metadata. + +### B3. Replace the README + +- [ ] Replace `README.md` (currently the starter's) with a library-style + README. Sections: + - Title + one-paragraph pitch ("Invoke Pipelex pipes from inside Mistral + Workflows activities"). + - Install: `pip install pipelex-mistralai-workflows`. Optional Temporal + layer: `pip install 'pipelex-mistralai-workflows[temporal]'`. + - Quick start (Tier 1): import `pipelex_run_pipe`, register on a worker, + call from a workflow. + - Per-call library scoping (Tier 2/3) using `library_crate_dump`. + - Streaming variant (`pipelex_run_pipe_streaming`). + - Migration note (mirror the CHANGELOG entry from Stream A, A10). + - Links: Pipelex docs, MTHDS standard, Mistral Workflows docs. + + Move the bulk of the deleted pipelex docs (`mistralai-workflows-plugin.md` + + `mistralai-workflows-recipes.md`) into the README — the docs site can + come later. Keep the README scannable; deeper recipes can become a + `docs/` subdirectory in a follow-up. + +### B4. Replace `CLAUDE.md` + +- [ ] Replace with a short repo-specific CLAUDE.md: + - Point at workspace `CLAUDE.md` for global rules. + - Note: do NOT depend on internal `pipelex` paths (e.g. anything under + `pipelex.pipe_run`, `pipelex.libraries`, etc.). Only depend on the + public `pipelex.embedding.*` surface. + - List the same `make agent-check` / `make agent-test` / `cleanderived` + workflow used in pipelex. + - Mirror pipelex's "No backward compatibility" rule. + +### B5. Rewrite `CHANGELOG.md` + +- [ ] Replace existing `[v0.8.0]` placeholder. New top-of-file: + + ```markdown + # Changelog + + ## [Unreleased] + + ## [v0.1.0] - + + First release. Extracts the Mistral Workflows ↔ Pipelex bridge from + `pipelex[mistralai-workflows]` into a dedicated package. + + ### Added + + - `pipelex_mistralai_workflows.activities.pipelex_run_pipe` — pre-decorated + Mistral Workflows activity wrapping `pipelex.embedding.run_pipe_via_bridge`. + - `pipelex_mistralai_workflows.activities.pipelex_run_pipe_offloaded` — + offload-capable variant for payloads that exceed Temporal's per-event + size limit. + - `pipelex_mistralai_workflows.streaming.pipelex_run_pipe_streaming` — + streaming activity that wraps the run in a Mistral `Task` + (`custom_task_type="pipelex.pipe_run"`) so subscribers see + `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / + `CustomTaskFailed` events. Emits per-step `CustomTaskInProgress` + events for `DIRECT` execution mode. + - `pipelex_mistralai_workflows.dependency.pipelex_dependency` — Mistral + component / dependency wrapper around `ensure_pipelex_booted` (see + §0.5 — final shape TBD pending a read of + `mistralai.workflows.plugins.mistralai`). + + ### Changed + + - Migrated from `pipelex.plugins.mistralai_workflows.*` to + `pipelex_mistralai_workflows.*`. Framework-agnostic types + (`PipelexPipeRunInput`, `PipelexPipeRunOutput`, + `run_pipe_via_bridge`, `PipelexExecutionMode`, + `ensure_pipelex_booted`) now imported from `pipelex.embedding.*`. + ``` + + Carry the three original landing-narrative bullets from the old + pipelex `[Unreleased]` (deleted in A10) into the **Added** section + above, rewriting the import paths to the new namespaces. + +### B6. Audit `.github/workflows/` + +The starter shipped 8 workflows. Verify each is fit-for-purpose: + +- [ ] `tests-check.yml` — the test job needs to install `pipelex[temporal]` + via the new `[temporal]` extra so layer-3 tests run. Confirm the + install step uses `uv sync --extra temporal` (or equivalent). +- [ ] `lint-check.yml` — should already work (calls `make` targets). +- [ ] `package-check.yml` — verifies the wheel builds; no changes. +- [ ] `version-check.yml` — verifies version bumps follow SemVer; review + that it works for a non-app library. +- [ ] `changelog-check.yml` — verifies CHANGELOG was updated on PRs; + confirm format expected matches B5. +- [ ] `cla.yml`, `guard-branches.yml`, `github-release.yml` — generic; keep + as-is, confirm they reference the right repo. + +If any workflow assumes starter conventions that don't apply, prune. + +### B7. Audit `Makefile` + +- [ ] Confirm all targets resolve in the new dep layout. Specifically: + - `make agent-check` should work without `mistralai-workflows`-specific + knowledge (it's a hard dep now). + - `make validate` calls `pipelex validate --all` — works only if the + package directory contains valid `.mthds` (currently the starter's + `hello_world.mthds` is being deleted in B1; layer-2 tests carry their + own `bridge_test.mthds` under `tests/integration/test_data/`). Decide + whether `make validate` is meaningful for this repo. Default: keep the + target; it's a no-op when there are no `.mthds` in the package. + +### B8. Refresh `uv.lock` + +- [ ] After B2 lands, run inside the new repo: + + ```bash + uv lock + uv sync --all-extras + ``` + + Commit the updated `uv.lock`. + +--- + +## Stream C — Move plugin code into the new repo + +Coordinated with the pipelex deletions in Stream A. Land Stream A's PR and +Stream C's first commit in lockstep so `git bisect` always builds. + +### C1. Move `activities.py` + +- [ ] Move `pipelex/plugins/mistralai_workflows/activities.py` → + `pipelex_mistralai_workflows/activities.py`. +- [ ] **Drop the optional-dep guard.** Replace: + + ```python + try: + from mistralai.workflows import activity + from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel + except ImportError as exc: + msg = (...) + raise MistralWorkflowsNotInstalledError(msg) from exc + ``` + + with bare imports: + + ```python + from mistralai.workflows import activity + from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel + ``` +- [ ] Rewrite Pipelex imports: + - `from pipelex.plugins.mistralai_workflows.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` + → `from pipelex.embedding.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` + - Drop the `from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError` import (exception deleted). + +### C2. Move `streaming.py` + +- [ ] Move `pipelex/plugins/mistralai_workflows/streaming.py` → + `pipelex_mistralai_workflows/streaming.py`. +- [ ] Drop the optional-dep guard (same pattern as C1). +- [ ] Rewrite imports: + - `pipelex.plugins.mistralai_workflows.bridge` → `pipelex.embedding.bridge` + - `pipelex.plugins.mistralai_workflows.execution_mode` → `pipelex.embedding.execution_mode` + - `pipelex.plugins.mistralai_workflows.streaming_event_forwarder` → `pipelex_mistralai_workflows.streaming_event_forwarder` + - Drop the `MistralWorkflowsNotInstalledError` import. + +### C3. Move `streaming_event_forwarder.py` + +- [ ] Move `pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py` + → `pipelex_mistralai_workflows/streaming_event_forwarder.py`. The + file has no `mistralai.workflows` imports and no + `pipelex.plugins.mistralai_workflows` imports — it's already + framework-agnostic. No edits needed beyond placement. +- [ ] Optionally: keep the writer_id `"mistralai-workflows-streaming"` + verbatim — it's a stable identifier that downstream observers may + already key off of. + +### C4. Add the Mistral component / dependency wrapper + +- [ ] Create `pipelex_mistralai_workflows/dependency.py`. Before writing + it, **read** `mistralai/workflows/plugins/mistralai` (the + reference plugin) and mirror its dependency-component shape. +- [ ] Provide at minimum: + - `pipelex_dependency` — a callable shaped for + `mistralai.workflows.Depends(...)`. Body wraps + `ensure_pipelex_booted()` (imported from `pipelex.embedding.bootstrap`) + and returns `Pipelex.get_instance()`. This is the function previously + living as `get_pipelex_dependency()` in + `pipelex.plugins.mistralai_workflows.bootstrap` (deleted in Stream A, + A4) — port it over with the Mistral-specific docstring. +- [ ] Optional: a `LibraryCrate` snapshot helper exposing + `library_crate_dump` per-call without forcing every caller to + hand-roll the `LibraryCrate.model_dump(...)` call. Defer if the + reference plugin doesn't follow this pattern. + +### C5. Move integration tests (layer-2 / layer-3) + +For each file, move from +`_workflows/tests/integration/pipelex/plugins/mistralai_workflows/` +to `pipelex-mistralai-workflows/tests/integration/`. + +- [ ] `test_activities_direct.py` +- [ ] `test_activities_offloaded.py` +- [ ] `test_activities_streaming.py` +- [ ] `test_bridge_temporal_blocking.py` +- [ ] `test_bridge_temporal_fire_and_forget.py` + +For each, rewrite imports: + +- `from pipelex.plugins.mistralai_workflows.bridge import ...` + → `from pipelex.embedding.bridge import ...` +- `from pipelex.plugins.mistralai_workflows.execution_mode import ...` + → `from pipelex.embedding.execution_mode import ...` +- `from pipelex.plugins.mistralai_workflows.activities import ...` + → `from pipelex_mistralai_workflows.activities import ...` +- `from pipelex.plugins.mistralai_workflows.streaming import ...` + → `from pipelex_mistralai_workflows.streaming import ...` +- `from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs import ...` + → `from tests.integration.test_data.bridge_funcs import ...` + +### C6. Move test fixtures + +- [ ] Copy + `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` → + `pipelex-mistralai-workflows/tests/integration/conftest.py`. The new + conftest needs to **merge** with the existing scaffold conftest + (which has `check_pipelex_initialized` and + `reset_pipelex_config_fixture`). Strategy: + - Keep the scaffold's `check_pipelex_initialized` and + `reset_pipelex_config_fixture` (session/module-scoped Pipelex setup). + - Add `bridge_test_library` (class-scoped) from the pipelex conftest. + - Update its import: `from tests.integration.test_data.bridge_funcs import ...`. +- [ ] Copy + `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` + → `pipelex-mistralai-workflows/tests/integration/test_data/`: + - `bridge_test.mthds` + - `bridge_funcs.py` + + Note: the same files also live in pipelex at + `tests/integration/pipelex/embedding/test_data/` (per Stream A, A8) for + the layer-1 bridge test. This is intentional duplication: both repos + exercise the same fixture against different layers. If divergence + becomes a maintenance problem later, factor into a tiny shared package; + for v0.1.0 keep duplicated. + +### C7. Verify the new repo + +- [ ] In `../pipelex-mistralai-workflows/`: + + ```bash + make install + make agent-check + make agent-test + ``` +- [ ] Run the layer-3 (Temporal) tests explicitly with the `temporal` extra: + + ```bash + .venv/bin/uv sync --extra temporal --extra dev + .venv/bin/pytest tests/integration/test_bridge_temporal_blocking.py \ + tests/integration/test_bridge_temporal_fire_and_forget.py + ``` +- [ ] Run the streaming layer-2 test with detailed logging to verify the + per-step `CustomTaskInProgress` event flow still asserts correctly: + + ```bash + .venv/bin/pytest -s tests/integration/test_activities_streaming.py + ``` +- [ ] Smoke import in a fresh shell: + + ```python + from pipelex_mistralai_workflows.activities import pipelex_run_pipe, pipelex_run_pipe_offloaded + from pipelex_mistralai_workflows.streaming import pipelex_run_pipe_streaming + from pipelex.embedding.bridge import PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge + from pipelex.embedding.execution_mode import PipelexExecutionMode + from pipelex.embedding.bootstrap import ensure_pipelex_booted + ``` + All six imports succeed without warnings. + +### C8. First release + +- [ ] Tag `v0.1.0` in `pipelex-mistralai-workflows`. +- [ ] Push the tag and create the GitHub release (use `release` skill if + available in the new repo, else manual). +- [ ] Publish to PyPI as `pipelex-mistralai-workflows==0.1.0`. +- [ ] Coordinate timing: this PyPI release ships **together with** the + pipelex release that drops the `[mistralai-workflows]` extra (Stream + A, A11 / migration paragraph in CHANGELOG). + +--- + +## Stream D — Coordinated landing & follow-ups + +### D1. Coordinated land + +- [ ] Land Stream A's PR on `pipelex` and ship the matching pipelex release + (containing the `pipelex.embedding` package and the migration + paragraph in CHANGELOG). +- [ ] On the same day, push `pipelex-mistralai-workflows==0.1.0` to PyPI + pinning `pipelex>=NEXT` to the freshly-released pipelex version. + +### D2. Cookbook entry (deferred from Phase 1.3) + +- [ ] In `pipelex-cookbook/`, create + `examples/c_advanced/mistral-workflows/`: + - Tier-1 DIRECT-mode worker script (using `pipelex_run_pipe` from the + new package). + - Tier-2 typed activity exercising `library_crate_dump`. + - README pointing back at the new repo's docs. +- [ ] Update `mistral-workflows-sub-module.md` §Status board to check off + the deferred Phase 1.3 cookbook entry. + +### D3. Watch the open risks + +- [ ] **Version coupling.** Document the `pipelex.embedding` public surface + as stable in pipelex docs. A breaking change to that surface is a + breaking change for the plugin pkg. +- [ ] **OffloadableField import drift.** `activities.py` (now in the new + repo) imports `OffloadableField, OffloadableModel` from + `mistralai.workflows.core.encoding.fields_offloader`. If a Mistral + upgrade moves the path, fix in the plugin pkg. +- [ ] **CI test parity.** Layer-2/3 tests now run only in + `pipelex-mistralai-workflows` CI. Make sure both repos' matrices are + green before flipping the switch (i.e. before merging Stream A's PR + to `main` and publishing v0.1.0). + +### D4. Workspace docs + +- [ ] Update root workspace `CLAUDE.md`'s repository table to include + `pipelex-mistralai-workflows/` (PyPI: `pipelex-mistralai-workflows`, + Python package: `pipelex_mistralai_workflows`). + +--- + +## Resume guide + +If you're picking this up cold: + +1. Read `wip/mistral-workflows-sub-module.md` §2 and §4 — the binding + design decisions and gotchas. Treat as spec; don't re-derive. +2. Read `wip/mistral-workflows-plugin-extract.md` end-to-end — the + strategy. This file (`TODOS.md`) is the execution layer. +3. Resolve §0 pre-decisions if not already locked. Defaults are usable. +4. Pick a stream: + - Streams A and B are independent — run in parallel. + - Stream C waits on both A and B. + - Stream D waits on C. +5. After every step: `make agent-check && make agent-test` in whichever + repo you touched. From 2ac7ebd1dad75ffb058ccdd21bb5e2c9c1d14244 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 11:03:52 +0200 Subject: [PATCH 11/16] Update TODOs for Mistral Workflows plugin extraction with new reference docs and clarify package structure --- TODOS.md | 149 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 93 insertions(+), 56 deletions(-) diff --git a/TODOS.md b/TODOS.md index 54d4a1a5b..33247d479 100644 --- a/TODOS.md +++ b/TODOS.md @@ -18,6 +18,23 @@ PyPI name is `pipelex-mistralai-workflows`. The scaffold version sits at `0.8.0` (inherited from the starter); we will reset to `0.1.0` as the first real release of this project. +**Reference docs (consult these before writing Mistral-facing code)** + +- Mistral Workflows skill (this repo): `.claude/skills/workflows/SKILL.md`. + Especially: + - `references/guides/workflows-plugins.mdx` — the plugin contract (most + relevant to §0.5 and Stream C, task C4). + - `references/guides/dependency-injection.mdx` — `Depends(...)` shape + (relevant to C4). + - `references/guides/streaming.mdx` + `references/guides/streaming-consumption.mdx` + — Task API, `update_state`, event subscription (relevant to Stream C, + task C2). + - `references/guides/handling-large-data.mdx` — `OffloadableField` and + the offloading interceptor (relevant to C1 and the open + `OffloadableField` import-drift risk in §D3). +- Mistral docs: + — official plugin authoring guide. + --- ## 0. Pre-decisions (lock these before writing code) @@ -25,15 +42,15 @@ real release of this project. Defaults below are the recommended path. Override only if there's a concrete reason; otherwise proceed. -- [ ] **0.1 — Where the framework-agnostic core lives.** Default: `pipelex/embedding/`. - Free package name (verified — no clash with existing modules; the - "embedding" hits in pipelex are unrelated HTML / jinja2 string usages). - The name communicates *embedding the Pipelex runtime into another - host runtime*. If the vector-embedding overlap feels confusing later, - `pipelex.runtime_bridge` is the fallback. +- [x] **0.1 — Framework-agnostic core lives at `pipelex/runtime_bridge/`** + (decision locked). The earlier `pipelex.embedding` proposal was + rejected: "embedding" overlaps too heavily with vector embeddings + and is misleading from the first read. `runtime_bridge` says what + the package actually does — bridge the Pipelex runtime into a host + runtime (Mistral Workflows, raw Temporal, future plugins). - [ ] **0.2 — Mistral-specific bits stay in the new repo, agnostic bits move - to `pipelex.embedding`.** Concrete split: - - **Move to `pipelex/embedding/`:** `bridge.py`, `execution_mode.py`, + to `pipelex.runtime_bridge`.** Concrete split: + - **Move to `pipelex/runtime_bridge/`:** `bridge.py`, `execution_mode.py`, `bootstrap.py::ensure_pipelex_booted`, the agnostic exceptions (`PipelexBridgeRuntimeError`, `MissingPipelexTemporalExtraError`). - **Move to `pipelex_mistralai_workflows/`:** `activities.py`, @@ -48,13 +65,33 @@ reason; otherwise proceed. - [ ] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** Currently `0.8.0` (starter inheritance) — that version space is wrong for a brand-new project. First release ships as `v0.1.0`. -- [ ] **0.4 — Pin `pipelex>=NEXT` in the new repo.** `NEXT` is whatever - pipelex version lands the `pipelex.embedding` package. Bump the - minimum on every pipelex release that touches the embedding surface. +- [ ] **0.4 — Pin `pipelex>=NEXT` in the new repo, plus an editable + `[tool.uv.sources]` override for local dev.** `NEXT` is whatever + pipelex version lands the `pipelex.runtime_bridge` package. Bump the + minimum on every pipelex release that touches the bridge surface. Independent SemVer for the plugin pkg. -- [ ] **0.5 — Mistral component / dependency wrapper shape.** Open: read - `mistralai.workflows.plugins.mistralai` (the reference plugin) before - committing to a shape. Stream C task C4 below holds the placeholder. + + Until `v0.1.0` ships, `pipelex-mistralai-workflows` must consume + `pipelex` from this worktree so edits to `pipelex/runtime_bridge/` + are picked up immediately by the plugin's tests: + + ```toml + # ../pipelex-mistralai-workflows/pyproject.toml + [tool.uv.sources] + pipelex = { path = "../_workflows", editable = true } + ``` + + Strip this override before publishing `v0.1.0` — PyPI builds must + resolve `pipelex` from PyPI, not a relative path. (Add the override + back on the next dev cycle when the next breaking change to + `pipelex.runtime_bridge` lands.) +- [ ] **0.5 — Mistral component / dependency wrapper shape.** Read the + Mistral plugin docs first: + `.claude/skills/workflows/references/guides/workflows-plugins.mdx` + and . + Cross-check `references/guides/dependency-injection.mdx` for the + `Depends(...)` shape. Stream C task C4 below holds the + implementation placeholder. - [ ] **0.6 — Cookbook entry timing.** Defer `pipelex-cookbook/examples/c_advanced/mistral-workflows/` until after `pipelex-mistralai-workflows==0.1.0` is on PyPI (Stream D). @@ -65,59 +102,59 @@ reason; otherwise proceed. Goal: end state where `git grep mistralai_workflows` and `git grep mistralai-workflows` both return zero hits inside `pipelex/`, and the -framework-agnostic core lives at `pipelex.embedding.*`. +framework-agnostic core lives at `pipelex.runtime_bridge.*`. ### A1. Create the new package -- [ ] Create `pipelex/embedding/` with an empty `__init__.py` (no +- [ ] Create `pipelex/runtime_bridge/` with an empty `__init__.py` (no re-exports — Pipelex rule). ### A2. Move `bridge.py` - [ ] Move `pipelex/plugins/mistralai_workflows/bridge.py` → - `pipelex/embedding/bridge.py`. + `pipelex/runtime_bridge/bridge.py`. - [ ] Rewrite imports inside `bridge.py`: - `from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted` - → `from pipelex.embedding.bootstrap import ensure_pipelex_booted` + → `from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted` - `from pipelex.plugins.mistralai_workflows.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` - → `from pipelex.embedding.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` + → `from pipelex.runtime_bridge.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` - `from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode` - → `from pipelex.embedding.execution_mode import PipelexExecutionMode` + → `from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode` - [ ] Rename the per-call library id prefix on line 222: `f"mistralai_workflows_{uuid4().hex[:8]}"` → - `f"embedding_{uuid4().hex[:8]}"`. + `f"runtime_bridge_{uuid4().hex[:8]}"`. - [ ] Update the install hint in `_require_pipelex_temporal_extra` (line 336): `"pip install 'pipelex[temporal,mistralai-workflows]'"` → `"pip install 'pipelex[temporal]'"`. - [ ] Update the module docstring: drop "of the mistralai_workflows plugin", - reframe as "framework-agnostic Pipelex embedding surface for host - runtimes (Mistral Workflows, raw Temporal, future plugins)". + reframe as "framework-agnostic Pipelex runtime-bridge surface for + host runtimes (Mistral Workflows, raw Temporal, future plugins)". ### A3. Move `execution_mode.py` - [ ] Move `pipelex/plugins/mistralai_workflows/execution_mode.py` → - `pipelex/embedding/execution_mode.py`. No import changes inside the + `pipelex/runtime_bridge/execution_mode.py`. No import changes inside the file. ### A4. Move `bootstrap.py` (split — keep agnostic, drop Mistral-shaped) - [ ] Move `pipelex/plugins/mistralai_workflows/bootstrap.py` → - `pipelex/embedding/bootstrap.py`. + `pipelex/runtime_bridge/bootstrap.py`. - [ ] Keep `ensure_pipelex_booted(...)` verbatim. Update the module docstring: drop "for use inside Mistral Workflows activities", reframe as "for use inside any host runtime that embeds Pipelex". -- [ ] **Delete** `get_pipelex_dependency()` from `pipelex/embedding/bootstrap.py` +- [ ] **Delete** `get_pipelex_dependency()` from `pipelex/runtime_bridge/bootstrap.py` — it returns a callable explicitly shaped for `mistralai.workflows.Depends` and belongs in the new repo. Its replacement lives in `pipelex_mistralai_workflows/dependency.py` (Stream C, task C4). ### A5. Split `exceptions.py` -- [ ] Create `pipelex/embedding/exceptions.py` with: - - `PipelexEmbeddingError(PipelexError)` — new base (replaces +- [ ] Create `pipelex/runtime_bridge/exceptions.py` with: + - `PipelexRuntimeBridgeError(PipelexError)` — new base (replaces `MistralWorkflowsPluginError`). - - `MissingPipelexTemporalExtraError(PipelexEmbeddingError)`. - - `PipelexBridgeRuntimeError(PipelexEmbeddingError)`. + - `MissingPipelexTemporalExtraError(PipelexRuntimeBridgeError)`. + - `PipelexBridgeRuntimeError(PipelexRuntimeBridgeError)`. - [ ] **Do NOT** carry `MistralWorkflowsNotInstalledError` over — it goes away entirely (the new repo has `mistralai-workflows>=3.3.0` as a hard dep, so the optional-dep guard pattern is obsolete). @@ -145,31 +182,31 @@ framework-agnostic core lives at `pipelex.embedding.*`. ### A8. Move/delete tests -Layer-1 (framework-agnostic) tests follow the embedding core into pipelex. +Layer-1 (framework-agnostic) tests follow the runtime-bridge core into pipelex. Layer-2 / layer-3 tests (which actually instantiate Mistral `WorkflowEnvironment` / activities) go to the new repo via Stream C. - [ ] **Move** `tests/unit/pipelex/plugins/mistralai_workflows/` → - `tests/unit/pipelex/embedding/`: + `tests/unit/pipelex/runtime_bridge/`: - `test_input_models.py` - `test_execution_mode.py` - `test_validation.py` - `test_dispatch.py` - In each, rewrite `pipelex.plugins.mistralai_workflows.*` imports → - `pipelex.embedding.*`. + `pipelex.runtime_bridge.*`. - [ ] **Move** the layer-1 integration test: `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` - → `tests/integration/pipelex/embedding/test_bridge_direct.py`. + → `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py`. Rewrite imports. - [ ] **Move the conftest + test_data with it.** They are needed by the layer-1 test that stays in pipelex AND will be copied to the new repo (Stream C, C6) for the layer-2 / layer-3 tests: - `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` - → `tests/integration/pipelex/embedding/conftest.py`. Update the import + → `tests/integration/pipelex/runtime_bridge/conftest.py`. Update the import path inside (`from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs` - → `from tests.integration.pipelex.embedding.test_data.bridge_funcs`). + → `from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs`). - `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` - → `tests/integration/pipelex/embedding/test_data/` (`bridge_test.mthds` + → `tests/integration/pipelex/runtime_bridge/test_data/` (`bridge_test.mthds` + `bridge_funcs.py`). - Update the `domain` in `bridge_test.mthds` if the prefix `mistralai_workflows_bridge_test` reads weirdly post-move; suggest @@ -218,10 +255,10 @@ landing. Replace with the migration story. > `pipelex.plugins.mistralai_workflows.*` modules have been removed > from `pipelex`. Install the new package instead: > `pip install pipelex-mistralai-workflows`, and import from - > `pipelex_mistralai_workflows.*`. The framework-agnostic embedding + > `pipelex_mistralai_workflows.*`. The framework-agnostic runtime-bridge > core (boundary types, `run_pipe_via_bridge`, `PipelexExecutionMode`, > `ensure_pipelex_booted`) has been promoted from - > `pipelex.plugins.mistralai_workflows.*` to `pipelex.embedding.*` so + > `pipelex.plugins.mistralai_workflows.*` to `pipelex.runtime_bridge.*` so > any host runtime — not just Mistral Workflows — can embed Pipelex. > No behavior changes; activities, boundary types, and execution > modes are identical. @@ -238,7 +275,7 @@ landing. Replace with the migration story. - [ ] `git grep mistralai-workflows pipelex/ tests/ pyproject.toml` returns only the migration paragraph in `CHANGELOG.md` and the install hint in `_require_pipelex_temporal_extra` (now removed per A2 — verify). -- [ ] `git grep "pipelex.embedding" pipelex/ tests/` finds the new package +- [ ] `git grep "pipelex.runtime_bridge" pipelex/ tests/` finds the new package paths. ### A12. (Out-of-scope reminder) Verify "make agent-check passes without optional dep" @@ -284,7 +321,7 @@ to a library distribution. ```toml dependencies = [ - "pipelex>=NEXT", # NEXT = the version that ships pipelex.embedding + "pipelex>=NEXT", # NEXT = the version that ships pipelex.runtime_bridge "mistralai-workflows>=3.3.0", ] ``` @@ -346,7 +383,7 @@ to a library distribution. - Point at workspace `CLAUDE.md` for global rules. - Note: do NOT depend on internal `pipelex` paths (e.g. anything under `pipelex.pipe_run`, `pipelex.libraries`, etc.). Only depend on the - public `pipelex.embedding.*` surface. + public `pipelex.runtime_bridge.*` surface. - List the same `make agent-check` / `make agent-test` / `cleanderived` workflow used in pipelex. - Mirror pipelex's "No backward compatibility" rule. @@ -368,7 +405,7 @@ to a library distribution. ### Added - `pipelex_mistralai_workflows.activities.pipelex_run_pipe` — pre-decorated - Mistral Workflows activity wrapping `pipelex.embedding.run_pipe_via_bridge`. + Mistral Workflows activity wrapping `pipelex.runtime_bridge.run_pipe_via_bridge`. - `pipelex_mistralai_workflows.activities.pipelex_run_pipe_offloaded` — offload-capable variant for payloads that exceed Temporal's per-event size limit. @@ -389,7 +426,7 @@ to a library distribution. `pipelex_mistralai_workflows.*`. Framework-agnostic types (`PipelexPipeRunInput`, `PipelexPipeRunOutput`, `run_pipe_via_bridge`, `PipelexExecutionMode`, - `ensure_pipelex_booted`) now imported from `pipelex.embedding.*`. + `ensure_pipelex_booted`) now imported from `pipelex.runtime_bridge.*`. ``` Carry the three original landing-narrative bullets from the old @@ -467,7 +504,7 @@ Stream C's first commit in lockstep so `git bisect` always builds. ``` - [ ] Rewrite Pipelex imports: - `from pipelex.plugins.mistralai_workflows.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` - → `from pipelex.embedding.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` + → `from pipelex.runtime_bridge.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` - Drop the `from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError` import (exception deleted). ### C2. Move `streaming.py` @@ -476,8 +513,8 @@ Stream C's first commit in lockstep so `git bisect` always builds. `pipelex_mistralai_workflows/streaming.py`. - [ ] Drop the optional-dep guard (same pattern as C1). - [ ] Rewrite imports: - - `pipelex.plugins.mistralai_workflows.bridge` → `pipelex.embedding.bridge` - - `pipelex.plugins.mistralai_workflows.execution_mode` → `pipelex.embedding.execution_mode` + - `pipelex.plugins.mistralai_workflows.bridge` → `pipelex.runtime_bridge.bridge` + - `pipelex.plugins.mistralai_workflows.execution_mode` → `pipelex.runtime_bridge.execution_mode` - `pipelex.plugins.mistralai_workflows.streaming_event_forwarder` → `pipelex_mistralai_workflows.streaming_event_forwarder` - Drop the `MistralWorkflowsNotInstalledError` import. @@ -500,7 +537,7 @@ Stream C's first commit in lockstep so `git bisect` always builds. - [ ] Provide at minimum: - `pipelex_dependency` — a callable shaped for `mistralai.workflows.Depends(...)`. Body wraps - `ensure_pipelex_booted()` (imported from `pipelex.embedding.bootstrap`) + `ensure_pipelex_booted()` (imported from `pipelex.runtime_bridge.bootstrap`) and returns `Pipelex.get_instance()`. This is the function previously living as `get_pipelex_dependency()` in `pipelex.plugins.mistralai_workflows.bootstrap` (deleted in Stream A, @@ -525,9 +562,9 @@ to `pipelex-mistralai-workflows/tests/integration/`. For each, rewrite imports: - `from pipelex.plugins.mistralai_workflows.bridge import ...` - → `from pipelex.embedding.bridge import ...` + → `from pipelex.runtime_bridge.bridge import ...` - `from pipelex.plugins.mistralai_workflows.execution_mode import ...` - → `from pipelex.embedding.execution_mode import ...` + → `from pipelex.runtime_bridge.execution_mode import ...` - `from pipelex.plugins.mistralai_workflows.activities import ...` → `from pipelex_mistralai_workflows.activities import ...` - `from pipelex.plugins.mistralai_workflows.streaming import ...` @@ -554,7 +591,7 @@ For each, rewrite imports: - `bridge_funcs.py` Note: the same files also live in pipelex at - `tests/integration/pipelex/embedding/test_data/` (per Stream A, A8) for + `tests/integration/pipelex/runtime_bridge/test_data/` (per Stream A, A8) for the layer-1 bridge test. This is intentional duplication: both repos exercise the same fixture against different layers. If divergence becomes a maintenance problem later, factor into a tiny shared package; @@ -587,9 +624,9 @@ For each, rewrite imports: ```python from pipelex_mistralai_workflows.activities import pipelex_run_pipe, pipelex_run_pipe_offloaded from pipelex_mistralai_workflows.streaming import pipelex_run_pipe_streaming - from pipelex.embedding.bridge import PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge - from pipelex.embedding.execution_mode import PipelexExecutionMode - from pipelex.embedding.bootstrap import ensure_pipelex_booted + from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge + from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted ``` All six imports succeed without warnings. @@ -610,7 +647,7 @@ For each, rewrite imports: ### D1. Coordinated land - [ ] Land Stream A's PR on `pipelex` and ship the matching pipelex release - (containing the `pipelex.embedding` package and the migration + (containing the `pipelex.runtime_bridge` package and the migration paragraph in CHANGELOG). - [ ] On the same day, push `pipelex-mistralai-workflows==0.1.0` to PyPI pinning `pipelex>=NEXT` to the freshly-released pipelex version. @@ -628,7 +665,7 @@ For each, rewrite imports: ### D3. Watch the open risks -- [ ] **Version coupling.** Document the `pipelex.embedding` public surface +- [ ] **Version coupling.** Document the `pipelex.runtime_bridge` public surface as stable in pipelex docs. A breaking change to that surface is a breaking change for the plugin pkg. - [ ] **OffloadableField import drift.** `activities.py` (now in the new From 00fe293c95179c24463a68306be30076da4dae91 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 11:09:37 +0200 Subject: [PATCH 12/16] Update TODOs for Mistral Workflows plugin extraction with version pinning and clarification on line references --- TODOS.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/TODOS.md b/TODOS.md index 33247d479..37e8a0d2e 100644 --- a/TODOS.md +++ b/TODOS.md @@ -35,6 +35,13 @@ real release of this project. - Mistral docs: — official plugin authoring guide. +**Line numbers in this file are hints, not anchors.** When this doc cites +`pyproject.toml` line 88 or `mkdocs.yml` lines 310–311, those numbers +reflect the state at write-time. If unrelated PRs land first, the lines +shift. The **descriptive text** (e.g. "the +`mistralai-workflows = [...]` entry under `[project.optional-dependencies]`") +is the source of truth — grep for it, don't jump to a stale line number. + --- ## 0. Pre-decisions (lock these before writing code) @@ -65,11 +72,13 @@ reason; otherwise proceed. - [ ] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** Currently `0.8.0` (starter inheritance) — that version space is wrong for a brand-new project. First release ships as `v0.1.0`. -- [ ] **0.4 — Pin `pipelex>=NEXT` in the new repo, plus an editable - `[tool.uv.sources]` override for local dev.** `NEXT` is whatever - pipelex version lands the `pipelex.runtime_bridge` package. Bump the - minimum on every pipelex release that touches the bridge surface. - Independent SemVer for the plugin pkg. +- [x] **0.4 — Pin `pipelex>=0.27.0` in the new repo, plus an editable + `[tool.uv.sources]` override for local dev.** `0.27.0` is the + pipelex release that lands `pipelex.runtime_bridge` (chosen as a + minor bump because the extraction is a breaking change for users + of the old `pipelex.plugins.mistralai_workflows.*` import path). + Bump the minimum on every pipelex release that touches the bridge + surface. Independent SemVer for the plugin pkg. Until `v0.1.0` ships, `pipelex-mistralai-workflows` must consume `pipelex` from this worktree so edits to `pipelex/runtime_bridge/` @@ -321,7 +330,7 @@ to a library distribution. ```toml dependencies = [ - "pipelex>=NEXT", # NEXT = the version that ships pipelex.runtime_bridge + "pipelex>=0.27.0", # 0.27.0 is the version that ships pipelex.runtime_bridge "mistralai-workflows>=3.3.0", ] ``` @@ -331,7 +340,7 @@ to a library distribution. ```toml [project.optional-dependencies] - temporal = ["pipelex[temporal]>=NEXT"] + temporal = ["pipelex[temporal]>=0.27.0"] ``` - [ ] Add the PEP 695 mypy override that pipelex used to carry — Mistral's source still uses PEP 695 type parameters mypy rejects under the @@ -650,7 +659,7 @@ For each, rewrite imports: (containing the `pipelex.runtime_bridge` package and the migration paragraph in CHANGELOG). - [ ] On the same day, push `pipelex-mistralai-workflows==0.1.0` to PyPI - pinning `pipelex>=NEXT` to the freshly-released pipelex version. + pinning `pipelex>=0.27.0` to match the freshly-released pipelex. ### D2. Cookbook entry (deferred from Phase 1.3) From ac765c2f8fc21fc44335bd9fcd8b97f476e6e389 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 12:16:27 +0200 Subject: [PATCH 13/16] Refactor and expand Pipelex runtime bridge for Mistral Workflows integration - Introduced a new `pipelex.runtime_bridge` package with core components for executing Pipelex pipes in various modes (DIRECT, TEMPORAL_BLOCKING, TEMPORAL_FIRE_AND_FORGET). - Added `ensure_pipelex_booted` function for idempotent initialization of Pipelex. - Created input/output models (`PipelexPipeRunInput`, `PipelexPipeRunOutput`) for structured data handling. - Implemented error handling with custom exceptions for runtime bridge operations. - Developed comprehensive integration tests for the runtime bridge, covering direct execution and library crate handling. - Updated documentation and TODOs to reflect the new structure and functionality. --- TODOS.md | 282 +++++++++++---- pipelex/runtime_bridge/__init__.py | 0 pipelex/runtime_bridge/bootstrap.py | 25 ++ pipelex/runtime_bridge/bridge.py | 339 ++++++++++++++++++ pipelex/runtime_bridge/exceptions.py | 13 + pipelex/runtime_bridge/execution_mode.py | 37 ++ .../pipelex/runtime_bridge/conftest.py | 35 ++ .../runtime_bridge/test_bridge_direct.py | 119 ++++++ .../runtime_bridge/test_data/bridge_funcs.py | 14 + .../test_data/bridge_test.mthds | 56 +++ .../pipelex/runtime_bridge/test_dispatch.py | 121 +++++++ .../runtime_bridge/test_execution_mode.py | 18 + .../runtime_bridge/test_input_models.py | 69 ++++ .../pipelex/runtime_bridge/test_validation.py | 75 ++++ 14 files changed, 1128 insertions(+), 75 deletions(-) create mode 100644 pipelex/runtime_bridge/__init__.py create mode 100644 pipelex/runtime_bridge/bootstrap.py create mode 100644 pipelex/runtime_bridge/bridge.py create mode 100644 pipelex/runtime_bridge/exceptions.py create mode 100644 pipelex/runtime_bridge/execution_mode.py create mode 100644 tests/integration/pipelex/runtime_bridge/conftest.py create mode 100644 tests/integration/pipelex/runtime_bridge/test_bridge_direct.py create mode 100644 tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py create mode 100644 tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds create mode 100644 tests/unit/pipelex/runtime_bridge/test_dispatch.py create mode 100644 tests/unit/pipelex/runtime_bridge/test_execution_mode.py create mode 100644 tests/unit/pipelex/runtime_bridge/test_input_models.py create mode 100644 tests/unit/pipelex/runtime_bridge/test_validation.py diff --git a/TODOS.md b/TODOS.md index 37e8a0d2e..defb22036 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,5 +1,159 @@ # Mistral Workflows ↔ Pipelex — Plugin Extraction TODOs +> **Session pause notes (2026-05-07).** Stream A is partially executed, +> Stream B is partially executed, Stream C has not yet been written. +> See **§Progress snapshot** below before resuming. Do NOT delete the +> source files in `pipelex/plugins/mistralai_workflows/` or the layer-2/3 +> tests yet — Stream C still depends on them. The source has not been +> deleted; only the layer-1 tests have been duplicated, and the new repo +> has been partially scaffolded. Resume guide is at the bottom of this +> file; the §Progress snapshot is the resume entry point. + +--- + +## Progress snapshot — what was done this session + +**Files written / moved (Stream A inside `_workflows/`):** + +- `pipelex/runtime_bridge/__init__.py` (empty) — created. +- `pipelex/runtime_bridge/exceptions.py` — created with + `PipelexRuntimeBridgeError` (new base), `MissingPipelexTemporalExtraError`, + `PipelexBridgeRuntimeError`. `MistralWorkflowsNotInstalledError` was + intentionally NOT carried over. +- `pipelex/runtime_bridge/execution_mode.py` — verbatim copy of the + original (no edits needed inside the file). +- `pipelex/runtime_bridge/bootstrap.py` — kept `ensure_pipelex_booted`, + rewrote module docstring, **deleted** `get_pipelex_dependency` (will be + reimplemented in `pipelex_mistralai_workflows/dependency.py` per Stream C + task C4). +- `pipelex/runtime_bridge/bridge.py` — moved with imports rewritten + (`pipelex.plugins.mistralai_workflows.*` → `pipelex.runtime_bridge.*`), + library-id prefix changed to `runtime_bridge_`, install hint changed to + `pip install 'pipelex[temporal]'`, module docstring rewritten as + framework-agnostic. + +**Tests moved (Stream A task A8 — layer-1 only):** + +- `tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py` + → `tests/unit/pipelex/runtime_bridge/test_input_models.py` (imports + rewritten). +- `tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py` + → `tests/unit/pipelex/runtime_bridge/test_execution_mode.py`. +- `tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py` + → `tests/unit/pipelex/runtime_bridge/test_validation.py`. +- `tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py` + → `tests/unit/pipelex/runtime_bridge/test_dispatch.py`. +- `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` + → `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py`. +- `tests/integration/pipelex/runtime_bridge/conftest.py` — new copy with + rewritten test_data import path + (`from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs ...`). +- `tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py` — + copy with updated docstring path. +- `tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds` + — verbatim copy (`domain = "mistralai_workflows_bridge_test"` kept + unchanged to minimize churn). + +**The originals at `pipelex/plugins/mistralai_workflows/` and +`tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` HAVE NOT +BEEN DELETED.** They still exist and tests would currently fail with +duplicate-collection / domain conflicts if run as-is. See "What's blocking +right now" below. + +**Files written in `../pipelex-mistralai-workflows/` (Stream B partial):** + +- `pyproject.toml` — fully rewritten per task B2 (version `0.1.0`, + description, authors, urls, deps slimmed to `pipelex>=0.27.0` + + `mistralai-workflows>=3.3.0`, `[project.optional-dependencies] temporal`, + `[[tool.mypy.overrides]]` for `mistralai.workflows.*`, dev extras with + `pytest-mock`, markers slimmed to `gha_disabled` / `dry_runnable` / + `temporal`, `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }`). +- `CLAUDE.md` — fully rewritten per task B4. +- `README.md` — fully rewritten per task B3. + +## What's blocking right now + +1. **Stream C has not been written.** The new repo's + `pipelex_mistralai_workflows/` still contains the starter + `hello_world.{py,mthds}`. The runtime files (`activities.py`, + `streaming.py`, `streaming_event_forwarder.py`, `dependency.py`) and + the layer-2/3 integration tests + fixtures still need to be created + in the new repo. +2. **Old source not yet deleted in `_workflows/`.** Layer-2/3 tests still + reference `pipelex.plugins.mistralai_workflows.*`. If you run tests + now you'll get collection errors / domain conflicts on the duplicated + `bridge_test.mthds` (same `domain = "mistralai_workflows_bridge_test"` + loaded twice — once from each location). Resolution: delete the old + plugin dir + old tests dir AFTER Stream C copies layer-2/3 over. +3. **`make agent-check` / `make agent-test` not yet run.** Pyright will + currently complain about both `pipelex.runtime_bridge.*` (cache + staleness — files exist) and the still-present old plugin dir. Run + `make cleanderived` first when resuming. + +## What to do next, in order + +1. **Stream C — write the runtime files in the new repo.** Use the file + contents already in this conversation (or read from + `pipelex/plugins/mistralai_workflows/{activities,streaming,streaming_event_forwarder}.py`) + and write them to `pipelex_mistralai_workflows/`, dropping the optional-dep + guard and rewriting Pipelex imports to `pipelex.runtime_bridge.*`. Add + `pipelex_mistralai_workflows/dependency.py` (task C4) with the + `pipelex_dependency` callable shaped for `mistralai.workflows.Depends(...)`. +2. **Stream C — write the layer-2/3 integration tests + fixtures in the + new repo.** Read sources and write to + `pipelex-mistralai-workflows/tests/integration/{test_*.py,conftest.py,test_data/}` + with rewritten imports per TODOS task C5/C6. The new repo's existing + `tests/integration/conftest.py` (with `check_pipelex_initialized`, + `reset_pipelex_config_fixture`) needs to be merged with a new + `bridge_test_library` fixture pulled from the pipelex conftest. +3. **Stream B finish-up.** + - B1: delete `pipelex_mistralai_workflows/hello_world.py`, + `pipelex_mistralai_workflows/hello_world.mthds`, + `tests/test_pipelines/`, `tests/e2e/test_pipelex_mistralai_workflows.py`. + - B5: rewrite `CHANGELOG.md` (currently still the starter's `[v0.8.0]` + placeholder). + - B6: audit `.github/workflows/tests-check.yml` — install step needs + to also install the `[temporal]` extra so layer-3 tests run. + - B7: audit `Makefile`. Default decision per TODOS: keep as-is. + - B8: `uv lock` + `uv sync --all-extras` and commit `uv.lock`. +4. **Stream A finish-up (in `_workflows/`).** + - A6: delete `pipelex/plugins/mistralai_workflows/` ENTIRELY (only + after Stream C has copied `activities.py` / `streaming.py` / + `streaming_event_forwarder.py` to the new repo). + - A8 finish: delete the now-redundant + `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs + entirely. + - A7: `pyproject.toml` — drop the `mistralai-workflows = [...]` entry + in `[project.optional-dependencies]` (currently line 88) AND the + `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` (currently + lines 154–164). + - A9: delete `docs/under-the-hood/mistralai-workflows-plugin.md` and + `docs/under-the-hood/mistralai-workflows-recipes.md`. Remove the four + `mkdocs.yml` lines (currently at lines 310, 311, 500, 501). + - A10: rewrite the `[Unreleased]` section of `CHANGELOG.md` per + existing TODOS task A10. + - A11: run `make cleanderived && make agent-check && make agent-test`. + Verify the four `git grep` invariants in TODOS A11. + +## Open questions / decisions the next session should NOT re-derive + +- **Pre-decisions §0.1 / §0.4 are locked.** The framework-agnostic core + lives at `pipelex.runtime_bridge.*`; pipelex-mistralai-workflows pins + `pipelex>=0.27.0`; the editable `[tool.uv.sources]` override is in place. +- **§0.3 is implemented.** New repo version is now `0.1.0`. +- **§0.2 is locked** but the splits in `pipelex_mistralai_workflows/` (the + Mistral-shaped DI helper, etc.) are NOT yet realized in code — that's + Stream C task C4. +- **§0.5 (Mistral component / dependency wrapper shape)** is still + unresolved. Read `mistralai.workflows.plugins.mistralai` before writing + C4. The placeholder is just a callable returning + `Pipelex.get_instance()` after `ensure_pipelex_booted()`. +- **§0.6 (cookbook entry timing).** Deferred — do not block on it. + +--- + +## Original execution plan (unchanged below this line) + Concrete execution plan for the migration described in `wip/mistral-workflows-plugin-extract.md`. Read that file plus the binding design decisions in `wip/mistral-workflows-sub-module.md` §2 and §4 @@ -115,56 +269,60 @@ framework-agnostic core lives at `pipelex.runtime_bridge.*`. ### A1. Create the new package -- [ ] Create `pipelex/runtime_bridge/` with an empty `__init__.py` (no +- [x] Create `pipelex/runtime_bridge/` with an empty `__init__.py` (no re-exports — Pipelex rule). ### A2. Move `bridge.py` -- [ ] Move `pipelex/plugins/mistralai_workflows/bridge.py` → - `pipelex/runtime_bridge/bridge.py`. -- [ ] Rewrite imports inside `bridge.py`: +- [x] Move `pipelex/plugins/mistralai_workflows/bridge.py` → + `pipelex/runtime_bridge/bridge.py`. **(Done as a copy — original + not yet deleted; A6 deletes the source dir.)** +- [x] Rewrite imports inside `bridge.py`: - `from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted` → `from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted` - `from pipelex.plugins.mistralai_workflows.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` → `from pipelex.runtime_bridge.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` - `from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode` → `from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode` -- [ ] Rename the per-call library id prefix on line 222: +- [x] Rename the per-call library id prefix on line 222: `f"mistralai_workflows_{uuid4().hex[:8]}"` → `f"runtime_bridge_{uuid4().hex[:8]}"`. -- [ ] Update the install hint in `_require_pipelex_temporal_extra` (line 336): +- [x] Update the install hint in `_require_pipelex_temporal_extra`: `"pip install 'pipelex[temporal,mistralai-workflows]'"` → `"pip install 'pipelex[temporal]'"`. -- [ ] Update the module docstring: drop "of the mistralai_workflows plugin", +- [x] Update the module docstring: drop "of the mistralai_workflows plugin", reframe as "framework-agnostic Pipelex runtime-bridge surface for host runtimes (Mistral Workflows, raw Temporal, future plugins)". ### A3. Move `execution_mode.py` -- [ ] Move `pipelex/plugins/mistralai_workflows/execution_mode.py` → +- [x] Move `pipelex/plugins/mistralai_workflows/execution_mode.py` → `pipelex/runtime_bridge/execution_mode.py`. No import changes inside the - file. + file. Docstring slightly reframed away from Mistral-specific wording. + **(Done as a copy — original not yet deleted.)** ### A4. Move `bootstrap.py` (split — keep agnostic, drop Mistral-shaped) -- [ ] Move `pipelex/plugins/mistralai_workflows/bootstrap.py` → - `pipelex/runtime_bridge/bootstrap.py`. -- [ ] Keep `ensure_pipelex_booted(...)` verbatim. Update the module +- [x] Move `pipelex/plugins/mistralai_workflows/bootstrap.py` → + `pipelex/runtime_bridge/bootstrap.py`. **(Done as a copy — original + not yet deleted.)** +- [x] Keep `ensure_pipelex_booted(...)` verbatim. Update the module docstring: drop "for use inside Mistral Workflows activities", reframe as "for use inside any host runtime that embeds Pipelex". -- [ ] **Delete** `get_pipelex_dependency()` from `pipelex/runtime_bridge/bootstrap.py` +- [x] **Delete** `get_pipelex_dependency()` from `pipelex/runtime_bridge/bootstrap.py` — it returns a callable explicitly shaped for `mistralai.workflows.Depends` and belongs in the new repo. Its replacement lives in `pipelex_mistralai_workflows/dependency.py` (Stream C, task C4). + **(The new file omits the function entirely.)** ### A5. Split `exceptions.py` -- [ ] Create `pipelex/runtime_bridge/exceptions.py` with: +- [x] Create `pipelex/runtime_bridge/exceptions.py` with: - `PipelexRuntimeBridgeError(PipelexError)` — new base (replaces `MistralWorkflowsPluginError`). - `MissingPipelexTemporalExtraError(PipelexRuntimeBridgeError)`. - `PipelexBridgeRuntimeError(PipelexRuntimeBridgeError)`. -- [ ] **Do NOT** carry `MistralWorkflowsNotInstalledError` over — it goes +- [x] **Do NOT** carry `MistralWorkflowsNotInstalledError` over — it goes away entirely (the new repo has `mistralai-workflows>=3.3.0` as a hard dep, so the optional-dep guard pattern is obsolete). @@ -195,7 +353,7 @@ Layer-1 (framework-agnostic) tests follow the runtime-bridge core into pipelex. Layer-2 / layer-3 tests (which actually instantiate Mistral `WorkflowEnvironment` / activities) go to the new repo via Stream C. -- [ ] **Move** `tests/unit/pipelex/plugins/mistralai_workflows/` → +- [x] **Move** `tests/unit/pipelex/plugins/mistralai_workflows/` → `tests/unit/pipelex/runtime_bridge/`: - `test_input_models.py` - `test_execution_mode.py` @@ -203,11 +361,13 @@ Layer-2 / layer-3 tests (which actually instantiate Mistral - `test_dispatch.py` - In each, rewrite `pipelex.plugins.mistralai_workflows.*` imports → `pipelex.runtime_bridge.*`. -- [ ] **Move** the layer-1 integration test: + **(Done as copies — originals not yet deleted; deletion is the bullet + below.)** +- [x] **Move** the layer-1 integration test: `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` → `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py`. - Rewrite imports. -- [ ] **Move the conftest + test_data with it.** They are needed by the + Rewrite imports. **(Done as copy — original not yet deleted.)** +- [x] **Move the conftest + test_data with it.** They are needed by the layer-1 test that stays in pipelex AND will be copied to the new repo (Stream C, C6) for the layer-2 / layer-3 tests: - `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` @@ -216,21 +376,19 @@ Layer-2 / layer-3 tests (which actually instantiate Mistral → `from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs`). - `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` → `tests/integration/pipelex/runtime_bridge/test_data/` (`bridge_test.mthds` - + `bridge_funcs.py`). - - Update the `domain` in `bridge_test.mthds` if the prefix - `mistralai_workflows_bridge_test` reads weirdly post-move; suggest - keeping the existing domain string for the move PR to minimize churn, - rename in a follow-up if needed. Tests reference the literal pipe - refs so any rename must be coordinated. + + `bridge_funcs.py`). **(Domain string `mistralai_workflows_bridge_test` + kept verbatim per default in this section.)** - [ ] **Delete** the layer-2 / layer-3 integration tests (they move to the - new repo via Stream C): + new repo via Stream C — DO NOT delete until Stream C has copied them + over): - `test_activities_direct.py` - `test_activities_offloaded.py` - `test_activities_streaming.py` - `test_bridge_temporal_blocking.py` - `test_bridge_temporal_fire_and_forget.py` - [ ] Delete the now-empty - `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs. + `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs + (do this together with the layer-2/3 deletion above). ### A9. Move docs @@ -315,60 +473,34 @@ to a library distribution. ### B2. Rewrite `pyproject.toml` -- [ ] `version = "0.1.0"` (currently `0.8.0`). -- [ ] `description = "Mistral Workflows plugin for Pipelex — invoke Pipelex pipes from inside Mistral Workflows activities."` - (currently a placeholder). -- [ ] Uncomment `authors` and set to - `[{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }]` (matching - pipelex). -- [ ] Update `[project.urls]`: +- [x] `version = "0.1.0"` (currently `0.8.0`). +- [x] `description = "Mistral Workflows plugin for Pipelex — invoke Pipelex pipes from inside Mistral Workflows activities."` +- [x] `authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }]`. +- [x] Update `[project.urls]`: - `Homepage = "https://pipelex.com"` - `Repository = "https://github.com/Pipelex/pipelex-mistralai-workflows"` - `Documentation = "https://docs.pipelex.com/"` -- [ ] Replace `dependencies = ["pipelex[mistralai,anthropic,...]>=0.26.4"]` - with the slim library shape: - - ```toml - dependencies = [ - "pipelex>=0.27.0", # 0.27.0 is the version that ships pipelex.runtime_bridge - "mistralai-workflows>=3.3.0", - ] - ``` - - No inference / cloud extras. This is a library, not an app. -- [ ] Add an optional extra for the Temporal layer-3 tests: - - ```toml - [project.optional-dependencies] - temporal = ["pipelex[temporal]>=0.27.0"] - ``` -- [ ] Add the PEP 695 mypy override that pipelex used to carry — Mistral's - source still uses PEP 695 type parameters mypy rejects under the - project's `python_version`. Copy the block (lines 154–164 in - pipelex's old `pyproject.toml` — moved here in Stream A task A7): - - ```toml - [[tool.mypy.overrides]] - follow_imports = "skip" - ignore_errors = true - module = ["mistralai.workflows.*", "mistralai.workflows"] - ``` -- [ ] Add `pytest-asyncio>=0.24.0`, `pytest-mock>=3.14.0` to the `dev` +- [x] Replace `dependencies` with the slim library shape (`pipelex>=0.27.0` + + `mistralai-workflows>=3.3.0`). No inference / cloud extras. +- [x] Add the `[temporal]` optional extra + (`pipelex[temporal]>=0.27.0`). +- [x] Add the PEP 695 mypy override for `mistralai.workflows.*` / + `mistralai.workflows`. +- [x] Add `pytest-asyncio>=0.24.0`, `pytest-mock>=3.14.0` to the `dev` extra. -- [ ] **Pytest markers** — keep only the markers the test suite actually - uses. Drop `inference` / `llm` / `img_gen` / `extract` / `pipelex_api` - (the layer-2/3 tests don't call inference). Keep: - - `gha_disabled` - - `dry_runnable` - - `temporal: tests that require a Temporal server` (mirror pipelex's) -- [ ] Reconsider `requires-python`. The scaffold is `>=3.12,<3.15` (because - `mistralai-workflows` requires 3.12+). pipelex itself targets 3.10+. - Keep `>=3.12,<3.15` here — Mistral Workflows is the binding floor. - Confirm by checking `mistralai-workflows` PyPI metadata. +- [x] **Pytest markers** — keep only `gha_disabled`, `dry_runnable`, + `temporal: tests that require a Temporal server`. Dropped + `inference` / `llm` / `img_gen` / `extract` / `pipelex_api` / + `needs_output` / `codex_disabled`. +- [x] `requires-python = ">=3.12,<3.15"` (kept from scaffold — Mistral + Workflows is the binding floor). +- [x] `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }` + — temporary editable override per §0.4. Strip before publishing + v0.1.0. ### B3. Replace the README -- [ ] Replace `README.md` (currently the starter's) with a library-style +- [x] Replace `README.md` (currently the starter's) with a library-style README. Sections: - Title + one-paragraph pitch ("Invoke Pipelex pipes from inside Mistral Workflows activities"). @@ -388,7 +520,7 @@ to a library distribution. ### B4. Replace `CLAUDE.md` -- [ ] Replace with a short repo-specific CLAUDE.md: +- [x] Replace with a short repo-specific CLAUDE.md: - Point at workspace `CLAUDE.md` for global rules. - Note: do NOT depend on internal `pipelex` paths (e.g. anything under `pipelex.pipe_run`, `pipelex.libraries`, etc.). Only depend on the diff --git a/pipelex/runtime_bridge/__init__.py b/pipelex/runtime_bridge/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/pipelex/runtime_bridge/bootstrap.py b/pipelex/runtime_bridge/bootstrap.py new file mode 100644 index 000000000..12d4b07c6 --- /dev/null +++ b/pipelex/runtime_bridge/bootstrap.py @@ -0,0 +1,25 @@ +"""Idempotent Pipelex boot helpers for use inside any host runtime that embeds Pipelex. + +Pipelex's own ``Pipelex.make()`` raises if a singleton already exists. The +bridge boundary is a hot path that can be reached from many concurrent +activities (Mistral Workflows, raw Temporal, future plugins), so we wrap the +boot in an idempotent guard so callers don't have to think about it. +""" + +from typing import Any + +from pipelex.pipelex import Pipelex + + +def ensure_pipelex_booted( + config_overrides: dict[str, Any] | None = None, +) -> None: + """Boot Pipelex on first call; no-op if already initialized. + + Idempotent. Safe to call from inside an activity; safe to call from a + worker entry-point before activities start. If a Pipelex singleton was + already created externally (e.g. via the user's worker bootstrap), this + function adopts that singleton without re-initializing. + """ + if Pipelex.get_optional_instance() is None: + Pipelex.make(config_overrides=config_overrides) diff --git a/pipelex/runtime_bridge/bridge.py b/pipelex/runtime_bridge/bridge.py new file mode 100644 index 000000000..d3215f6c9 --- /dev/null +++ b/pipelex/runtime_bridge/bridge.py @@ -0,0 +1,339 @@ +"""Framework-agnostic Pipelex runtime-bridge surface for host runtimes. + +This module contains the boundary types (``PipelexPipeRunInput`` / +``PipelexPipeRunOutput``) and the dispatch entry-point +(``run_pipe_via_bridge``) used by host runtimes (Mistral Workflows, raw +Temporal, future plugins) to invoke Pipelex pipes from inside their own +activities. It deliberately does NOT import any host-runtime-specific +modules at module top-level so that callers can use the bridge directly +(Tier 3 usage) and so that unit tests can exercise it without optional +host-runtime deps installed. + +The Temporal extra is lazy-imported only inside the temporal-mode branches. +""" + +from __future__ import annotations + +from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, AsyncGenerator +from uuid import uuid4 + +import shortuuid +from pydantic import BaseModel, ConfigDict, Field + +from pipelex.core.memory.working_memory import MAIN_STUFF_NAME +from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory +from pipelex.hub import ( + get_library_manager, + get_required_pipe, + set_current_library, + teardown_current_library, +) +from pipelex.libraries.library_crate import LibraryCrate +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.pipe_run.exceptions import PipeJobError, PipeRouterError, PipeRunError +from pipelex.pipe_run.pipe_job_factory import PipeJobFactory +from pipelex.pipe_run.pipe_router import PipeRouter +from pipelex.pipe_run.pipe_run import PipeRun +from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory +from pipelex.pipeline.exceptions import PipeExecutionError, PipelineExecutionError +from pipelex.pipeline.job_metadata import JobMetadata +from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted +from pipelex.runtime_bridge.exceptions import ( + MissingPipelexTemporalExtraError, + PipelexBridgeRuntimeError, +) +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode +from pipelex.system.telemetry.otel_constants import OTelConstants + +if TYPE_CHECKING: + from pipelex.core.memory.working_memory import WorkingMemory + from pipelex.core.pipes.pipe_output import PipeOutput + from pipelex.graph.graph_context import GraphContext + from pipelex.pipe_run.pipe_job import PipeJob + + +class PipelexPipeRunInput(BaseModel): + """JSON-safe input crossing the host-runtime / Temporal boundary.""" + + model_config = ConfigDict(extra="forbid") + + pipe_code: str + inputs: dict[str, Any] = Field(default_factory=dict) + output_name: str | None = None + pipeline_run_id: str | None = None + user_id: str | None = None + library_crate_dump: dict[str, Any] | None = None + execution_mode: PipelexExecutionMode = PipelexExecutionMode.DIRECT + delivery_assignment_dump: dict[str, Any] | None = None + + +class PipelexPipeRunOutput(BaseModel): + """JSON-safe output crossing the host-runtime / Temporal boundary.""" + + model_config = ConfigDict(extra="forbid") + + output_dict: dict[str, Any] + main_stuff_name: str | None = None + pipeline_run_id: str + workflow_id: str | None = None + is_completed: bool + graph_spec_dump: dict[str, Any] | None = None + + +async def run_pipe_via_bridge( + input_payload: PipelexPipeRunInput, + graph_context: GraphContext | None = None, +) -> PipelexPipeRunOutput: + """Run a Pipelex pipe from inside a host-runtime activity. + + Booting Pipelex on first call (no-op if already initialized); validating + the input; opening a per-call scoped library if a ``library_crate_dump`` + is provided; then dispatching to the requested execution mode. + + The optional ``graph_context`` is plumbed into ``JobMetadata`` so callers + (e.g. a streaming activity) that already opened a ``GraphTracerManager`` + tracer for this pipeline run get per-step trace events flowing through + the configured event log. ``graph_context`` is only honored for + ``DIRECT`` execution mode — TEMPORAL modes already have their own + event-log infrastructure via ``pipeline_run_setup`` and a passed-in + context would be ignored anyway. + """ + ensure_pipelex_booted() + _validate_input(input_payload) + + library_crate = _decode_library_crate(input_payload.library_crate_dump) + delivery_assignment = _decode_delivery_assignment(input_payload.delivery_assignment_dump) + + async with _scoped_library_for_crate(library_crate): + pipe_job = build_pipe_job_from_input( + input_payload=input_payload, + library_crate=library_crate, + graph_context=graph_context, + ) + + match input_payload.execution_mode: + case PipelexExecutionMode.DIRECT: + return await _run_direct(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + case PipelexExecutionMode.TEMPORAL_BLOCKING: + _require_pipelex_temporal_extra() + return await _run_temporal_blocking(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + _require_pipelex_temporal_extra() + return await _run_temporal_fire_and_forget(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + + +def build_pipe_job_from_input( + input_payload: PipelexPipeRunInput, + library_crate: LibraryCrate | None, + graph_context: GraphContext | None = None, +) -> PipeJob: + """Hydrate a PipeJob from JSON-safe input. + + Looks up the pipe in the active library; the caller is responsible for + making sure the active library contains the pipe (by passing a + ``library_crate_dump`` or pre-loading the library at boot). + + The optional ``graph_context`` is plumbed into ``JobMetadata`` so a + caller (e.g. a streaming activity) that has already opened a + ``GraphTracerManager`` tracer for this pipeline run can have per-step + ``PipeStartEvent`` / ``PipeEndSuccessEvent`` events flow through the + pipe execution. When ``None``, no tracing happens (current default). + """ + pipe = get_required_pipe(pipe_code=input_payload.pipe_code) + + pipeline_run_id = input_payload.pipeline_run_id or shortuuid.uuid() + + working_memory: WorkingMemory + if input_payload.inputs: + working_memory = WorkingMemoryFactory.make_from_pipeline_inputs( + pipeline_inputs=input_payload.inputs, + search_domain_codes=[pipe.domain_code], + ) + else: + working_memory = WorkingMemoryFactory.make_empty() + + job_metadata = JobMetadata( + user_id=input_payload.user_id or OTelConstants.DEFAULT_USER_ID, + pipeline_run_id=pipeline_run_id, + graph_context=graph_context, + ) + pipe_run_params = PipeRunParamsFactory.make_run_params() + + return PipeJobFactory.make_pipe_job( + pipe=pipe, + pipe_run_params=pipe_run_params, + job_metadata=job_metadata, + working_memory=working_memory, + output_name=input_payload.output_name, + library_crate=library_crate, + ) + + +def serialize_pipe_output(pipe_output: PipeOutput) -> dict[str, Any]: + """Dehydrate a PipeOutput's working memory to a JSON-safe dict. + + Always uses ``WorkingMemory.dump_for_temporal()`` — the same format Pipelex + uses internally for Temporal transit. The shape is stable regardless of + whether a ``library_crate`` was attached: + ``{"root": {stuff_name: {"content": {...}, ...}}, "aliases": {...}}``. + + Type metadata embedded by ``dump_for_temporal`` lets callers reconstruct a + typed ``WorkingMemory`` when they have the matching class registry in + scope (e.g. via ``hydrate_working_memory``). + """ + return pipe_output.working_memory.dump_for_temporal() + + +def _validate_input(input_payload: PipelexPipeRunInput) -> None: + if input_payload.execution_mode is PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET and input_payload.delivery_assignment_dump is None: + msg = ( + "PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET requires a delivery_assignment_dump; " + "otherwise the pipe completion would be silently dropped." + ) + raise PipelexBridgeRuntimeError(msg) + + +def _decode_library_crate(library_crate_dump: dict[str, Any] | None) -> LibraryCrate | None: + if library_crate_dump is None: + return None + return LibraryCrate.model_validate(library_crate_dump) + + +def _decode_delivery_assignment(delivery_assignment_dump: dict[str, Any] | None) -> DeliveryAssignment | None: + if delivery_assignment_dump is None: + return None + return DeliveryAssignment.model_validate(delivery_assignment_dump) + + +@asynccontextmanager +async def _scoped_library_for_crate(library_crate: LibraryCrate | None) -> AsyncGenerator[str | None, None]: # noqa: RUF029 + """Open a per-call scoped library for the duration of a pipe run. + + When ``library_crate`` is None, this is a no-op: callers fall back to the + library that was loaded into the active class registry at boot. When + provided, opens a fresh library, loads the crate into it, sets it as the + current library for the duration of the pipe execution, and tears it down + on the way out. + """ + if library_crate is None: + yield None + return + + library_manager = get_library_manager() + library_id = f"runtime_bridge_{uuid4().hex[:8]}" + library_manager.open_library(library_id=library_id) + set_current_library(library_id=library_id) + try: + library_manager.load_from_crate(library_id=library_id, crate=library_crate) + yield library_id + finally: + library_manager.teardown(library_id=library_id) + teardown_current_library() + + +async def _run_direct( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + pipe_run = PipeRun(pipe_router=PipeRouter()) + try: + pipe_output = await pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe execution failed in DIRECT mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return _serialize_completed_output( + pipe_output=pipe_output, + pipe_job=pipe_job, + workflow_id=None, + ) + + +async def _run_temporal_blocking( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 + + temporal_pipe_run = make_temporal_pipe_run() + try: + pipe_output = await temporal_pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe execution failed in TEMPORAL_BLOCKING mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return _serialize_completed_output( + pipe_output=pipe_output, + pipe_job=pipe_job, + workflow_id=pipe_output.pipeline_run_id, + ) + + +async def _run_temporal_fire_and_forget( + pipe_job: PipeJob, + delivery_assignment: DeliveryAssignment | None, +) -> PipelexPipeRunOutput: + from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 + + temporal_pipe_run = make_temporal_pipe_run() + try: + workflow_id, _handle = await temporal_pipe_run.start(pipe_job=pipe_job, delivery_assignment=delivery_assignment) + except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: + msg = f"Pipe dispatch failed in TEMPORAL_FIRE_AND_FORGET mode for pipe '{pipe_job.pipe.code}': {exc}" + raise PipelexBridgeRuntimeError(msg) from exc + + return PipelexPipeRunOutput( + output_dict={}, + main_stuff_name=None, + pipeline_run_id=pipe_job.job_metadata.pipeline_run_id, + workflow_id=workflow_id, + is_completed=False, + graph_spec_dump=None, + ) + + +def _serialize_completed_output( + pipe_output: PipeOutput, + pipe_job: PipeJob, # noqa: ARG001 — kept for symmetry with future per-crate serialization tweaks + workflow_id: str | None, +) -> PipelexPipeRunOutput: + output_dict = serialize_pipe_output(pipe_output=pipe_output) + + main_stuff_name = _resolve_main_stuff_root_key(pipe_output=pipe_output) + + graph_spec_dump = pipe_output.graph_spec.model_dump(mode="json") if pipe_output.graph_spec is not None else None + + return PipelexPipeRunOutput( + output_dict=output_dict, + main_stuff_name=main_stuff_name, + pipeline_run_id=pipe_output.pipeline_run_id, + workflow_id=workflow_id, + is_completed=True, + graph_spec_dump=graph_spec_dump, + ) + + +def _resolve_main_stuff_root_key(pipe_output: PipeOutput) -> str | None: + """Return the actual ``root`` dict key under which the main stuff lives. + + The main stuff can either sit directly at ``root[MAIN_STUFF_NAME]`` or be + referenced via ``aliases[MAIN_STUFF_NAME]`` pointing at its real name. + Callers indexing the output_dict need the actual root key, not the + stuff's display ``stuff_name``. + """ + working_memory = pipe_output.working_memory + if MAIN_STUFF_NAME in working_memory.root: + return MAIN_STUFF_NAME + aliased_target = working_memory.aliases.get(MAIN_STUFF_NAME) + if aliased_target is not None and aliased_target in working_memory.root: + return aliased_target + return None + + +def _require_pipelex_temporal_extra() -> None: + try: + import temporalio # noqa: F401, PLC0415 + except ImportError as exc: + msg = "TEMPORAL_* execution modes require the pipelex[temporal] extra. Install with: pip install 'pipelex[temporal]'" + raise MissingPipelexTemporalExtraError(msg) from exc diff --git a/pipelex/runtime_bridge/exceptions.py b/pipelex/runtime_bridge/exceptions.py new file mode 100644 index 000000000..ba2faa12d --- /dev/null +++ b/pipelex/runtime_bridge/exceptions.py @@ -0,0 +1,13 @@ +from pipelex.base_exceptions import PipelexError + + +class PipelexRuntimeBridgeError(PipelexError): + """Base for errors raised by the Pipelex runtime-bridge surface.""" + + +class MissingPipelexTemporalExtraError(PipelexRuntimeBridgeError): + """Raised when a TEMPORAL_* execution mode is requested without the pipelex[temporal] extra.""" + + +class PipelexBridgeRuntimeError(PipelexRuntimeBridgeError): + """Raised when a pipe execution dispatched through the bridge fails.""" diff --git a/pipelex/runtime_bridge/execution_mode.py b/pipelex/runtime_bridge/execution_mode.py new file mode 100644 index 000000000..2dd4a7b73 --- /dev/null +++ b/pipelex/runtime_bridge/execution_mode.py @@ -0,0 +1,37 @@ +from pipelex.types import StrEnum + + +class PipelexExecutionMode(StrEnum): + """How a Pipelex pipe runs inside a host runtime activity. + + DIRECT: in-process; no Temporal involved on Pipelex's side; activity blocks + until the pipe completes. Fastest feedback, simplest ops. + TEMPORAL_BLOCKING: dispatch the pipe as a Pipelex Temporal workflow; the + activity awaits completion. Pipe runs durably on Pipelex's worker + fleet. Requires the pipelex[temporal] extra. + TEMPORAL_FIRE_AND_FORGET: dispatch the pipe as a Pipelex Temporal workflow + and return immediately with the workflow_id. Activity does NOT wait; + completion is signalled out-of-band via DeliveryAssignment (webhook / + storage). Same dep requirements as TEMPORAL_BLOCKING. + ``delivery_assignment_dump`` is required. + """ + + DIRECT = "direct" + TEMPORAL_BLOCKING = "temporal_blocking" + TEMPORAL_FIRE_AND_FORGET = "temporal_fire_and_forget" + + @property + def requires_pipelex_temporal(self) -> bool: + match self: + case PipelexExecutionMode.DIRECT: + return False + case PipelexExecutionMode.TEMPORAL_BLOCKING | PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + return True + + @property + def is_fire_and_forget(self) -> bool: + match self: + case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: + return True + case PipelexExecutionMode.DIRECT | PipelexExecutionMode.TEMPORAL_BLOCKING: + return False diff --git a/tests/integration/pipelex/runtime_bridge/conftest.py b/tests/integration/pipelex/runtime_bridge/conftest.py new file mode 100644 index 000000000..ae1e1d47b --- /dev/null +++ b/tests/integration/pipelex/runtime_bridge/conftest.py @@ -0,0 +1,35 @@ +from collections.abc import Generator +from pathlib import Path + +import pytest + +from pipelex.hub import get_func_registry, get_library_manager, set_current_library +from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs import mistralai_workflows_bridge_echo + +TEST_DATA_DIR = Path(__file__).parent / "test_data" + + +@pytest.fixture(scope="class") +def bridge_test_library() -> Generator[str, None, None]: + """Open a class-scoped library populated with the bridge test pipe. + + The pipe ``mistralai_workflows_bridge_test.bridge_func_pipe`` is registered + in the active library, and the matching Python function is registered in + the FuncRegistry. Both are torn down on exit. + """ + func_registry = get_func_registry() + func_registry.register_function(mistralai_workflows_bridge_echo) + + library_manager = get_library_manager() + library_id, _ = library_manager.open_library() + set_current_library(library_id=library_id) + library_manager.load_libraries( + library_id=library_id, + library_dirs=[TEST_DATA_DIR], + ) + try: + yield library_id + finally: + library_manager.teardown(library_id=library_id) + if func_registry.has_function("mistralai_workflows_bridge_echo"): + func_registry.unregister_function_by_name("mistralai_workflows_bridge_echo") diff --git a/tests/integration/pipelex/runtime_bridge/test_bridge_direct.py b/tests/integration/pipelex/runtime_bridge/test_bridge_direct.py new file mode 100644 index 000000000..20d6e45d9 --- /dev/null +++ b/tests/integration/pipelex/runtime_bridge/test_bridge_direct.py @@ -0,0 +1,119 @@ +"""Layer-1 integration tests for the runtime-bridge in DIRECT mode. + +These tests do NOT depend on any host-runtime optional package — they +exercise only the framework-agnostic core (``run_pipe_via_bridge`` with a +real loaded pipe). The Mistral Workflows activity wrapper is covered +separately in the ``pipelex-mistralai-workflows`` package, which DOES +require the optional dep. +""" + +from typing import Any + +import pytest + +from pipelex.hub import get_library_manager +from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, run_pipe_via_bridge +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + +PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" + + +@pytest.mark.asyncio(loop_scope="class") +class TestBridgeDirect: + async def test_direct_mode_with_globally_loaded_library( + self, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Bridge runs a pipe found in the active library when no crate is provided.""" + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "hello world"}, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.workflow_id is None + assert result.main_stuff_name is not None + main_stuff_dump = result.output_dict["root"][result.main_stuff_name] + assert main_stuff_dump["content"]["text"] == "hello world" + + async def test_direct_mode_with_library_crate_dump( + self, + bridge_test_library: str, + ) -> None: + """Bridge round-trips through ``library_crate_dump`` end-to-end. + + Captures a LibraryCrate from the loaded library, pipes it through the + bridge as a JSON-safe dict, and verifies the pipe still resolves and + runs against the per-call scoped library that the bridge opens. + """ + crate = get_library_manager().get_crate(library_id=bridge_test_library) + assert crate is not None + crate_dump: dict[str, Any] = crate.model_dump(mode="json") + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "via crate"}, + library_crate_dump=crate_dump, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.main_stuff_name is not None + main_stuff_dump = result.output_dict["root"][result.main_stuff_name] + assert main_stuff_dump["content"]["text"] == "via crate" + + async def test_direct_mode_uses_caller_pipeline_run_id( + self, + bridge_test_library: str, # noqa: ARG002 + ) -> None: + """Caller-supplied ``pipeline_run_id`` propagates to the PipeJob.""" + caller_run_id = "caller-supplied-run-id" + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=PIPE_REF, + inputs={"input_text": "trace me"}, + pipeline_run_id=caller_run_id, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.pipeline_run_id == caller_run_id + + async def test_direct_mode_dynamic_concept_round_trips_via_library_crate_dump( + self, + bridge_test_library: str, + ) -> None: + """A concept with an inline structure round-trips through ``library_crate_dump``. + + ``EchoEnvelope`` is defined inline in the bridge_test bundle. The bridge + dehydrates the library to a JSON-safe crate dump, opens a per-call + scoped library on the receiving side, and re-hydrates the concept so + ``PipeCompose`` can construct a ``StructuredContent`` matching the + dynamic shape. + """ + envelope_pipe_ref = "mistralai_workflows_bridge_test.bridge_envelope_pipe" + crate = get_library_manager().get_crate(library_id=bridge_test_library) + assert crate is not None + crate_dump: dict[str, Any] = crate.model_dump(mode="json") + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code=envelope_pipe_ref, + inputs={"input_text": "wrapped"}, + library_crate_dump=crate_dump, + execution_mode=PipelexExecutionMode.DIRECT, + ) + ) + + assert result.is_completed is True + assert result.main_stuff_name is not None + main_stuff = result.output_dict["root"][result.main_stuff_name] + content = main_stuff["content"] + assert content["text"] == "wrapped" + assert content["origin"] == "mistralai_workflows_bridge" diff --git a/tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py b/tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py new file mode 100644 index 000000000..e82e05d00 --- /dev/null +++ b/tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py @@ -0,0 +1,14 @@ +"""Test functions registered for the runtime-bridge integration tests.""" + +from pipelex.core.memory.working_memory import WorkingMemory +from pipelex.core.stuffs.text_content import TextContent + + +def mistralai_workflows_bridge_echo(working_memory: WorkingMemory) -> TextContent: + """Echo the ``input_text`` stuff back as a TextContent output. + + Used by tests/integration/pipelex/runtime_bridge to validate end-to-end + pipe execution through the bridge without invoking inference. + """ + input_text = working_memory.get_stuff_as_str("input_text") + return TextContent(text=input_text) diff --git a/tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds b/tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds new file mode 100644 index 000000000..9cc1f048c --- /dev/null +++ b/tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds @@ -0,0 +1,56 @@ +domain = "mistralai_workflows_bridge_test" +description = "Test pipes for the mistralai_workflows plugin bridge" + +[concept.EchoEnvelope] +description = "Custom dynamic concept used to exercise library_crate_dump round-trip for inline-structured concepts." + +[concept.EchoEnvelope.structure] +text = { type = "text", required = true, description = "The echoed text" } +origin = { type = "text", required = true, description = "Origin marker for the echo" } + +[pipe.bridge_func_pipe] +type = "PipeFunc" +description = "Echoes the input text back as output (DIRECT mode only — PipeFunc is not Temporal-compatible)" +output = "Text" +function_name = "mistralai_workflows_bridge_echo" + +[pipe.bridge_compose_pipe] +type = "PipeCompose" +description = "Echoes the input_text via a Jinja2 template (Temporal-compatible)" +inputs = { input_text = "Text" } +output = "Text" +template = "{{ input_text.text }}" + +[pipe.bridge_envelope_pipe] +type = "PipeCompose" +description = "Composes the input_text into a structured EchoEnvelope, exercising dynamic-concept round-trip via library_crate_dump" +inputs = { input_text = "Text" } +output = "EchoEnvelope" + +[pipe.bridge_envelope_pipe.construct] +text = { from = "input_text.text" } +origin = "mistralai_workflows_bridge" + +[pipe.bridge_seq_step_one] +type = "PipeCompose" +description = "First step of bridge_sequence_pipe — uppercases the input text" +inputs = { input_text = "Text" } +output = "Text" +template = "{{ input_text.text | upper }}" + +[pipe.bridge_seq_step_two] +type = "PipeCompose" +description = "Second step of bridge_sequence_pipe — wraps the result with markers" +inputs = { uppercased = "Text" } +output = "Text" +template = "[STEP2:{{ uppercased.text }}]" + +[pipe.bridge_sequence_pipe] +type = "PipeSequence" +description = "Two-step sequence used by Phase 2.1 streaming tests to assert per-step events" +inputs = { input_text = "Text" } +output = "Text" +steps = [ + { pipe = "bridge_seq_step_one", result = "uppercased" }, + { pipe = "bridge_seq_step_two", result = "final_text" }, +] diff --git a/tests/unit/pipelex/runtime_bridge/test_dispatch.py b/tests/unit/pipelex/runtime_bridge/test_dispatch.py new file mode 100644 index 000000000..6c9f3d92a --- /dev/null +++ b/tests/unit/pipelex/runtime_bridge/test_dispatch.py @@ -0,0 +1,121 @@ +import pytest +from pytest_mock import MockerFixture + +from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory +from pipelex.core.pipes.pipe_output import PipeOutput +from pipelex.pipe_run.pipe_job import PipeJob +from pipelex.pipe_run.pipe_run import PipeRun +from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory +from pipelex.pipeline.job_metadata import JobMetadata +from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, run_pipe_via_bridge +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + + +def _make_fake_pipe_job(mocker: MockerFixture, pipe_code: str, pipeline_run_id: str) -> PipeJob: + """Build a PipeJob without triggering Pydantic's PipeAbstract validation. + + Tests at the dispatch layer don't care about the concrete pipe — only + that the bridge routes the right pipe_job to the right PipeRun. Using + ``model_construct`` lets us pass a MagicMock as ``pipe`` without + constructing a full PipeAbstract subclass. + """ + fake_pipe = mocker.MagicMock() + fake_pipe.code = pipe_code + fake_pipe.domain_code = "fake_domain" + return PipeJob.model_construct( + pipe=fake_pipe, + working_memory=WorkingMemoryFactory.make_empty(), + pipe_run_params=PipeRunParamsFactory.make_run_params(), + job_metadata=JobMetadata(user_id="anonymous", pipeline_run_id=pipeline_run_id), + library_crate=None, + ) + + +@pytest.mark.asyncio +class TestDispatch: + async def test_direct_mode_calls_pipe_run_with_pipe_job(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.runtime_bridge.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_output = PipeOutput( + working_memory=WorkingMemoryFactory.make_empty(), + pipeline_run_id="injected-run-id", + ) + mock_run = mocker.patch.object(PipeRun, "run", new_callable=mocker.AsyncMock, return_value=fake_output) + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.DIRECT, + pipeline_run_id="caller-run-id", + ) + ) + + assert mock_run.await_count == 1 + await_args = mock_run.await_args + assert await_args is not None + call_kwargs: dict[str, object] = dict(await_args.kwargs) + assert call_kwargs["delivery_assignment"] is None + assert call_kwargs["pipe_job"] is fake_job + + assert result.is_completed is True + assert result.pipeline_run_id == "injected-run-id" + assert result.workflow_id is None + assert result.graph_spec_dump is None + + async def test_temporal_blocking_dispatches_to_temporal_pipe_run(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.runtime_bridge.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_output = PipeOutput( + working_memory=WorkingMemoryFactory.make_empty(), + pipeline_run_id="temporal-run-id", + ) + fake_temporal_run = mocker.AsyncMock(return_value=fake_output) + fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") + fake_factory.return_value.run = fake_temporal_run + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, + ) + ) + + fake_factory.assert_called_once() + assert fake_temporal_run.await_count == 1 + assert result.is_completed is True + assert result.workflow_id == "temporal-run-id" + + async def test_temporal_fire_and_forget_returns_workflow_id_without_completion(self, mocker: MockerFixture) -> None: + fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") + mocker.patch( + "pipelex.runtime_bridge.bridge.build_pipe_job_from_input", + return_value=fake_job, + ) + + fake_handle = mocker.MagicMock() + fake_start = mocker.AsyncMock(return_value=("wf-id-42", fake_handle)) + fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") + fake_factory.return_value.start = fake_start + + result = await run_pipe_via_bridge( + PipelexPipeRunInput( + pipe_code="fake_pipe", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump={"webhooks": [], "storage": None}, + pipeline_run_id="caller-run-id", + ) + ) + + fake_start.assert_awaited_once() + assert result.is_completed is False + assert result.workflow_id == "wf-id-42" + assert result.pipeline_run_id == "caller-run-id" + assert result.output_dict == {} diff --git a/tests/unit/pipelex/runtime_bridge/test_execution_mode.py b/tests/unit/pipelex/runtime_bridge/test_execution_mode.py new file mode 100644 index 000000000..6a1ea927d --- /dev/null +++ b/tests/unit/pipelex/runtime_bridge/test_execution_mode.py @@ -0,0 +1,18 @@ +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + + +class TestPipelexExecutionMode: + def test_string_values_are_stable(self): + assert PipelexExecutionMode.DIRECT == "direct" + assert PipelexExecutionMode.TEMPORAL_BLOCKING == "temporal_blocking" + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET == "temporal_fire_and_forget" + + def test_requires_pipelex_temporal(self): + assert PipelexExecutionMode.DIRECT.requires_pipelex_temporal is False + assert PipelexExecutionMode.TEMPORAL_BLOCKING.requires_pipelex_temporal is True + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.requires_pipelex_temporal is True + + def test_is_fire_and_forget(self): + assert PipelexExecutionMode.DIRECT.is_fire_and_forget is False + assert PipelexExecutionMode.TEMPORAL_BLOCKING.is_fire_and_forget is False + assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.is_fire_and_forget is True diff --git a/tests/unit/pipelex/runtime_bridge/test_input_models.py b/tests/unit/pipelex/runtime_bridge/test_input_models.py new file mode 100644 index 000000000..b710ba8d7 --- /dev/null +++ b/tests/unit/pipelex/runtime_bridge/test_input_models.py @@ -0,0 +1,69 @@ +import pytest +from pydantic import ValidationError + +from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, PipelexPipeRunOutput +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + + +class TestInputOutputModels: + def test_input_defaults_match_design(self): + payload = PipelexPipeRunInput(pipe_code="some_pipe") + assert payload.pipe_code == "some_pipe" + assert payload.inputs == {} + assert payload.output_name is None + assert payload.pipeline_run_id is None + assert payload.user_id is None + assert payload.library_crate_dump is None + assert payload.execution_mode is PipelexExecutionMode.DIRECT + assert payload.delivery_assignment_dump is None + + def test_input_forbids_extra_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunInput.model_validate( + { + "pipe_code": "some_pipe", + "unexpected": "field", + } + ) + + def test_input_requires_pipe_code(self): + with pytest.raises(ValidationError): + PipelexPipeRunInput.model_validate({}) + + def test_input_round_trip_via_json(self): + original = PipelexPipeRunInput( + pipe_code="some_pipe", + inputs={"foo": "bar"}, + execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, + pipeline_run_id="run-123", + user_id="alice", + ) + round_tripped = PipelexPipeRunInput.model_validate(original.model_dump(mode="json")) + assert round_tripped == original + + def test_output_required_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunOutput.model_validate({"output_dict": {}}) # missing pipeline_run_id and is_completed + + def test_output_forbids_extra_fields(self): + with pytest.raises(ValidationError): + PipelexPipeRunOutput.model_validate( + { + "output_dict": {}, + "pipeline_run_id": "run-1", + "is_completed": True, + "rogue_field": 42, + } + ) + + def test_output_round_trip_via_json(self): + original = PipelexPipeRunOutput( + output_dict={"foo": "bar"}, + main_stuff_name="main", + pipeline_run_id="run-1", + workflow_id=None, + is_completed=True, + graph_spec_dump=None, + ) + round_tripped = PipelexPipeRunOutput.model_validate(original.model_dump(mode="json")) + assert round_tripped == original diff --git a/tests/unit/pipelex/runtime_bridge/test_validation.py b/tests/unit/pipelex/runtime_bridge/test_validation.py new file mode 100644 index 000000000..3d114dd78 --- /dev/null +++ b/tests/unit/pipelex/runtime_bridge/test_validation.py @@ -0,0 +1,75 @@ +import pytest + +from pipelex.libraries.library_crate import LibraryCrate +from pipelex.pipe_run.delivery_assignment import DeliveryAssignment +from pipelex.runtime_bridge.bridge import ( + PipelexPipeRunInput, + _decode_delivery_assignment, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + _decode_library_crate, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + _validate_input, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] + run_pipe_via_bridge, +) +from pipelex.runtime_bridge.exceptions import PipelexBridgeRuntimeError +from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode + + +class TestBridgeValidationAndDecoding: + def test_validate_input_passes_for_direct_without_delivery(self): + payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.DIRECT) + _validate_input(payload) # must not raise + + def test_validate_input_passes_for_temporal_blocking_without_delivery(self): + payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING) + _validate_input(payload) # must not raise + + def test_validate_input_rejects_fire_and_forget_without_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + ) + with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): + _validate_input(payload) + + def test_validate_input_accepts_fire_and_forget_with_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + delivery_assignment_dump={"webhooks": [], "storage": None}, + ) + _validate_input(payload) # must not raise + + def test_decode_library_crate_returns_none_for_none(self): + assert _decode_library_crate(None) is None + + def test_decode_library_crate_round_trips_empty(self): + empty = LibraryCrate() + decoded = _decode_library_crate(empty.model_dump(mode="json")) + assert decoded is not None + assert decoded.concepts == empty.concepts + assert decoded.pipes == empty.pipes + + def test_decode_delivery_assignment_returns_none_for_none(self): + assert _decode_delivery_assignment(None) is None + + def test_decode_delivery_assignment_round_trips(self): + assignment = DeliveryAssignment.model_validate( + { + "storage": {"key_prefix": "runs/abc"}, + "webhooks": [{"url": "https://example.test/hook"}], + } + ) + decoded = _decode_delivery_assignment(assignment.model_dump(mode="json")) + assert decoded is not None + assert decoded.storage is not None + assert decoded.storage.key_prefix == "runs/abc/" # storage validator appends trailing / + assert len(decoded.webhooks) == 1 + assert decoded.webhooks[0].url == "https://example.test/hook" + + @pytest.mark.asyncio + async def test_run_pipe_via_bridge_rejects_fire_and_forget_without_delivery(self): + payload = PipelexPipeRunInput( + pipe_code="any", + execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, + ) + with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): + await run_pipe_via_bridge(payload) From 3d3081c17bd2d76f32f0d9c94144174654ae4c47 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 12:46:04 +0200 Subject: [PATCH 14/16] Refactor Mistral Workflows integration into a standalone package - Extracted Mistral Workflows integration from `pipelex` into a new package `pipelex-mistralai-workflows`, allowing for independent installation and usage. - Removed the `pipelex.plugins.mistralai_workflows` modules and updated the core runtime bridge to be framework-agnostic. - Updated `CHANGELOG.md` to reflect the changes and migration instructions for users. - Deleted outdated documentation and tests related to the previous plugin structure. - Ensured no behavioral changes; all existing functionalities remain intact under the new package structure. --- CHANGELOG.md | 6 +- TODOS.md | 880 ++++++------------ .../mistralai-workflows-plugin.md | 149 --- .../mistralai-workflows-recipes.md | 294 ------ mkdocs.yml | 4 - .../plugins/mistralai_workflows/__init__.py | 0 .../plugins/mistralai_workflows/activities.py | 94 -- .../plugins/mistralai_workflows/bootstrap.py | 39 - pipelex/plugins/mistralai_workflows/bridge.py | 337 ------- .../plugins/mistralai_workflows/exceptions.py | 17 - .../mistralai_workflows/execution_mode.py | 37 - .../plugins/mistralai_workflows/streaming.py | 226 ----- .../streaming_event_forwarder.py | 284 ------ pyproject.toml | 13 - .../plugins/mistralai_workflows/conftest.py | 35 - .../test_activities_direct.py | 112 --- .../test_activities_offloaded.py | 117 --- .../test_activities_streaming.py | 259 ------ .../mistralai_workflows/test_bridge_direct.py | 118 --- .../test_bridge_temporal_blocking.py | 149 --- .../test_bridge_temporal_fire_and_forget.py | 145 --- .../test_data/bridge_funcs.py | 14 - .../test_data/bridge_test.mthds | 56 -- .../mistralai_workflows/test_dispatch.py | 121 --- .../test_execution_mode.py | 18 - .../mistralai_workflows/test_input_models.py | 69 -- .../mistralai_workflows/test_validation.py | 75 -- 27 files changed, 278 insertions(+), 3390 deletions(-) delete mode 100644 docs/under-the-hood/mistralai-workflows-plugin.md delete mode 100644 docs/under-the-hood/mistralai-workflows-recipes.md delete mode 100644 pipelex/plugins/mistralai_workflows/__init__.py delete mode 100644 pipelex/plugins/mistralai_workflows/activities.py delete mode 100644 pipelex/plugins/mistralai_workflows/bootstrap.py delete mode 100644 pipelex/plugins/mistralai_workflows/bridge.py delete mode 100644 pipelex/plugins/mistralai_workflows/exceptions.py delete mode 100644 pipelex/plugins/mistralai_workflows/execution_mode.py delete mode 100644 pipelex/plugins/mistralai_workflows/streaming.py delete mode 100644 pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/conftest.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py delete mode 100644 tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds delete mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py delete mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py delete mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py delete mode 100644 tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f2d28992..6d476bcd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,9 @@ ## [Unreleased] -### Added +### Changed -- **Pipelex pipes can now be invoked from inside Mistral Workflows activities** via the new `pipelex.plugins.mistralai_workflows` plugin (optional dep `pipelex[mistralai-workflows]`). The plugin offers three usage tiers: a pre-decorated `pipelex_run_pipe` activity, a `run_pipe_via_bridge` helper to wrap in your own typed activity, and a low-level `build_pipe_job_from_input` / `serialize_pipe_output` API. Three execution modes via `PipelexExecutionMode`: `DIRECT` (in-process inside the activity), `TEMPORAL_BLOCKING` (dispatch to Pipelex's Temporal cluster, wait for result), and `TEMPORAL_FIRE_AND_FORGET` (dispatch and return immediately with a workflow id; completion delivered out-of-band via `DeliveryAssignment`). The boundary is JSON-only — no internal Pipelex types cross the activity surface — and per-call library scoping via `library_crate_dump` lets activities run pipes from bundles that aren't pre-loaded into the worker's global registry. A second activity `pipelex_run_pipe_offloaded` (with `PipelexPipeRunInputOffloaded` / `PipelexPipeRunOutputOffloaded` boundary types wrapped in `OffloadableField`) lets users plug into Mistral's `ActivityInOutOffloadingInterceptor` for payloads that exceed Temporal's per-event size limit. See `docs/under-the-hood/mistralai-workflows-plugin.md` for the architecture and `docs/under-the-hood/mistralai-workflows-recipes.md` for worked examples. -- **Streaming variant of the bridge activity** — `pipelex.plugins.mistralai_workflows.streaming.pipelex_run_pipe_streaming` wraps `run_pipe_via_bridge` in a single Mistral `Task` (`custom_task_type="pipelex.pipe_run"`) so subscribers get `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed` lifecycle events for each pipe run. The silent path (`pipelex_run_pipe`) is unchanged — opt into the streaming activity only when you need observability. -- **Per-step streaming for `DIRECT` mode** — when called with `PipelexExecutionMode.DIRECT`, `pipelex_run_pipe_streaming` now also emits one `CustomTaskInProgress` event per Pipelex pipe boundary (PipeStartEvent / PipeEndSuccessEvent / PipeEndErrorEvent), surfacing `current_step_pipe_code`, `current_step_node_id`, `started_steps`, `completed_steps`, and `last_output_stuff_name` on the streaming state. Implemented via a queue-backed `EventLogProtocol` injected into a per-call `GraphTracerManager` tracer plus an asyncio forwarder that translates trace events into `Task.update_state(...)` calls. `TEMPORAL_BLOCKING` / `TEMPORAL_FIRE_AND_FORGET` keep the previous single-pair semantics. +- **Mistral Workflows integration extracted into a dedicated package.** The optional `pipelex[mistralai-workflows]` extra and the `pipelex.plugins.mistralai_workflows.*` modules have been removed from `pipelex`. Install the new package instead: `pip install pipelex-mistralai-workflows`, and import from `pipelex_mistralai_workflows.*`. The framework-agnostic runtime-bridge core (boundary types, `run_pipe_via_bridge`, `PipelexExecutionMode`, `ensure_pipelex_booted`) has been promoted from `pipelex.plugins.mistralai_workflows.*` to `pipelex.runtime_bridge.*` so any host runtime — not just Mistral Workflows — can embed Pipelex. No behavior changes; activities, boundary types, and execution modes are identical. ## [v0.26.4] - 2026-05-06 diff --git a/TODOS.md b/TODOS.md index defb22036..e45dd85c0 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,154 +1,165 @@ -# Mistral Workflows ↔ Pipelex — Plugin Extraction TODOs - -> **Session pause notes (2026-05-07).** Stream A is partially executed, -> Stream B is partially executed, Stream C has not yet been written. -> See **§Progress snapshot** below before resuming. Do NOT delete the -> source files in `pipelex/plugins/mistralai_workflows/` or the layer-2/3 -> tests yet — Stream C still depends on them. The source has not been -> deleted; only the layer-1 tests have been duplicated, and the new repo -> has been partially scaffolded. Resume guide is at the bottom of this -> file; the §Progress snapshot is the resume entry point. +# Mistral Workflows ↔ Pipelex — Plugin Extraction TODOS + +> **Session status (2026-05-07).** Streams A, B, and C are **fully done and +> verified**. Both repos are green: +> - `pipelex-mistralai-workflows`: `make agent-check` clean, +> `make agent-test` passes (all 8 tests including 3 layer-2 Mistral +> activity tests + 2 layer-3 Temporal-marked tests + 2 fundamentals). +> - `pipelex` (`_workflows/`): `make agent-check` clean (pyright + mypy +> across 1708 files), `make agent-test` passes, all 4 git-grep invariants +> from A11 satisfied. +> +> **Stream D is the only remaining stream**: coordinated landing & PyPI +> publish (D1), cookbook (D2 deferred), risk-watch (D3), workspace docs +> update (D4). Resume entry point: §Stream D below. +> +> **One non-obvious gotcha discovered & fixed this session**: Mistral's +> `get_effective_task_queue()` returns `worker.deployment_name` (not +> `temporal.task_queue`) whenever `deployment_name` is set and doesn't match +> the configured task queue. A developer `.env` with +> `DEPLOYMENT_NAME=BatMac.local` (or anything else) silently routes +> activities to that deployment name, so the in-process test worker — which +> polls `TEST_TASK_QUEUE` — never picks them up and the workflow hangs +> forever. The fixture `override_mistralai_task_queue` in all 3 layer-2 test +> files now also clears `mistralai_config.worker.deployment_name = None`. +> This is the kind of thing Mistral may relax in a future release; if so, +> the override-to-None can become a no-op but should still be left in for +> safety. --- -## Progress snapshot — what was done this session - -**Files written / moved (Stream A inside `_workflows/`):** - -- `pipelex/runtime_bridge/__init__.py` (empty) — created. -- `pipelex/runtime_bridge/exceptions.py` — created with - `PipelexRuntimeBridgeError` (new base), `MissingPipelexTemporalExtraError`, - `PipelexBridgeRuntimeError`. `MistralWorkflowsNotInstalledError` was - intentionally NOT carried over. -- `pipelex/runtime_bridge/execution_mode.py` — verbatim copy of the - original (no edits needed inside the file). -- `pipelex/runtime_bridge/bootstrap.py` — kept `ensure_pipelex_booted`, - rewrote module docstring, **deleted** `get_pipelex_dependency` (will be - reimplemented in `pipelex_mistralai_workflows/dependency.py` per Stream C - task C4). -- `pipelex/runtime_bridge/bridge.py` — moved with imports rewritten - (`pipelex.plugins.mistralai_workflows.*` → `pipelex.runtime_bridge.*`), - library-id prefix changed to `runtime_bridge_`, install hint changed to - `pip install 'pipelex[temporal]'`, module docstring rewritten as - framework-agnostic. +## Progress snapshot — what was done across sessions + +### Done in `_workflows/` (Stream A) -**Tests moved (Stream A task A8 — layer-1 only):** - -- `tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py` - → `tests/unit/pipelex/runtime_bridge/test_input_models.py` (imports - rewritten). -- `tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py` - → `tests/unit/pipelex/runtime_bridge/test_execution_mode.py`. -- `tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py` - → `tests/unit/pipelex/runtime_bridge/test_validation.py`. -- `tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py` - → `tests/unit/pipelex/runtime_bridge/test_dispatch.py`. -- `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` - → `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py`. -- `tests/integration/pipelex/runtime_bridge/conftest.py` — new copy with - rewritten test_data import path - (`from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs ...`). -- `tests/integration/pipelex/runtime_bridge/test_data/bridge_funcs.py` — - copy with updated docstring path. -- `tests/integration/pipelex/runtime_bridge/test_data/bridge_test.mthds` - — verbatim copy (`domain = "mistralai_workflows_bridge_test"` kept - unchanged to minimize churn). - -**The originals at `pipelex/plugins/mistralai_workflows/` and -`tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` HAVE NOT -BEEN DELETED.** They still exist and tests would currently fail with -duplicate-collection / domain conflicts if run as-is. See "What's blocking -right now" below. - -**Files written in `../pipelex-mistralai-workflows/` (Stream B partial):** - -- `pyproject.toml` — fully rewritten per task B2 (version `0.1.0`, - description, authors, urls, deps slimmed to `pipelex>=0.27.0` + - `mistralai-workflows>=3.3.0`, `[project.optional-dependencies] temporal`, - `[[tool.mypy.overrides]]` for `mistralai.workflows.*`, dev extras with - `pytest-mock`, markers slimmed to `gha_disabled` / `dry_runnable` / - `temporal`, `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }`). -- `CLAUDE.md` — fully rewritten per task B4. -- `README.md` — fully rewritten per task B3. +- **A1–A5 (refactor inside pipelex)** — `pipelex/runtime_bridge/` package + fully populated with `__init__.py` (empty), `bridge.py`, `bootstrap.py` + (without `get_pipelex_dependency`), `execution_mode.py`, and `exceptions.py` + (with `PipelexRuntimeBridgeError` base + `MissingPipelexTemporalExtraError` + + `PipelexBridgeRuntimeError`; `MistralWorkflowsNotInstalledError` dropped). + All imports rewritten to `pipelex.runtime_bridge.*`. Library-id prefix + changed to `runtime_bridge_`. Install hint changed to + `pip install 'pipelex[temporal]'`. Module docstrings reframed as + framework-agnostic. +- **A6 — Old plugin dir deleted.** `pipelex/plugins/mistralai_workflows/` no + longer exists. +- **A7 — pyproject.toml updated.** The `mistralai-workflows = [...]` extra + removed from `[project.optional-dependencies]`. The + `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` removed. +- **A8 (layer-1 tests)** — `tests/unit/pipelex/runtime_bridge/` populated + with the four unit tests; `tests/integration/pipelex/runtime_bridge/` + populated with `test_bridge_direct.py` + `conftest.py` + `test_data/` + (`bridge_test.mthds` + `bridge_funcs.py`). The old + `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` directories + are deleted. +- **A9 — Docs removed.** `docs/under-the-hood/mistralai-workflows-plugin.md` + + `mistralai-workflows-recipes.md` deleted; the four `mkdocs.yml` lines + removed. +- **A10 — `[Unreleased]` rewritten.** The three plugin-landing bullets are + out; the migration paragraph is in (Stream A, Changed bullet). + +### Done in `pipelex-mistralai-workflows/` (Stream B + Stream C) + +- **B1 — Starter content stripped.** `hello_world.py`, `hello_world.mthds`, + `tests/test_pipelines/`, `tests/e2e/test_pipelex_mistralai_workflows.py` + all deleted. (Empty `tests/e2e/conftest.py` left in place.) +- **B2 — pyproject.toml fully rewritten** (`v0.1.0`, slim deps, `[temporal]` + extra, mypy override for `mistralai.workflows.*`, `pytest-mock` in dev, + pruned markers, `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }`). + Also added `pythonpath = ["tests"]` under `[tool.pytest]` so the + `from integration.test_data.bridge_funcs import ...` import in + `tests/integration/conftest.py` resolves at collection time + (project rule forbids `tests/__init__.py`). +- **B3 — README rewritten.** +- **B4 — CLAUDE.md rewritten.** +- **B5 — CHANGELOG rewritten.** `[Unreleased]` empty; `[v0.1.0]` populated + with the three landing bullets (rewritten paths) plus the Mistral + `Depends`-ready `pipelex_dependency` bullet, and a Changed bullet noting + the namespace migration. +- **B6 — CI audited.** `tests-check.yml` already calls `make install` → + `uv sync --all-extras`, which pulls in the `[temporal]` extra. No edits + needed. +- **B7 — Makefile audit.** Default decision honored: keep `make validate` + as-is. +- **B8 — `uv.lock` refreshed and committed-state.** `uv lock` then + `uv sync --all-extras` ran successfully; the editable `pipelex` install + works (`pipelex==0.26.4` from `file:///Users/lchoquel/repos/Pipelex/_workflows`). + Smoke-imported all six public symbols (`pipelex_run_pipe`, + `pipelex_run_pipe_offloaded`, `pipelex_run_pipe_streaming`, + plus the three `pipelex.runtime_bridge.*` paths) — all OK. +- **C1 — `activities.py` written** in the new repo. Optional-dep guard + dropped; bare imports of `mistralai.workflows.{activity,...}`. Pipelex + imports rewritten to `pipelex.runtime_bridge.bridge`. +- **C2 — `streaming.py` written.** Same edits; sibling import goes to + `pipelex_mistralai_workflows.streaming_event_forwarder`. +- **C3 — `streaming_event_forwarder.py` copied verbatim** (it had no + Mistral or pipelex.plugins imports already). +- **C4 — `dependency.py` written.** Single `pipelex_dependency()` callable + that returns a `Pipelex` instance, designed to be passed to + `mistralai.workflows.Depends(...)`. Booted on first resolve via + `ensure_pipelex_booted()`. (`§0.5` is now considered locked.) +- **C5 — Layer-2 / 3 integration tests moved.** All five test files live in + `pipelex-mistralai-workflows/tests/integration/` with imports rewritten + (`pipelex.plugins.mistralai_workflows.*` → `pipelex.runtime_bridge.*` + + `pipelex_mistralai_workflows.*`). +- **C6 — Test fixtures moved + conftest merged.** The new + `tests/integration/conftest.py` keeps the scaffold's + `check_pipelex_initialized` / `reset_pipelex_config_fixture` and adds + the `bridge_test_library` class-scoped fixture. Test data + (`bridge_test.mthds` + `bridge_funcs.py`) copied to + `tests/integration/test_data/`. Domain string + `mistralai_workflows_bridge_test` kept verbatim so the same `.mthds` file + works in both repos. Conftest imports use the + `from integration.test_data.bridge_funcs import ...` style backed by + `pythonpath = ["tests"]` (see B2). + +### Verified in this session + +- New repo `make agent-check` → **clean** (ruff format + lint, plxt format + + lint, pyright = 0 errors, mypy = no issues across 5 source files). +- `.env` for the new repo was added by the user to unblock pipelex boot + during tests (Langfuse public key was missing). +- New repo `make agent-test` was kicked off in background **but had not + finished by the time this pause was written** — see "What's blocking + right now" below. ## What's blocking right now -1. **Stream C has not been written.** The new repo's - `pipelex_mistralai_workflows/` still contains the starter - `hello_world.{py,mthds}`. The runtime files (`activities.py`, - `streaming.py`, `streaming_event_forwarder.py`, `dependency.py`) and - the layer-2/3 integration tests + fixtures still need to be created - in the new repo. -2. **Old source not yet deleted in `_workflows/`.** Layer-2/3 tests still - reference `pipelex.plugins.mistralai_workflows.*`. If you run tests - now you'll get collection errors / domain conflicts on the duplicated - `bridge_test.mthds` (same `domain = "mistralai_workflows_bridge_test"` - loaded twice — once from each location). Resolution: delete the old - plugin dir + old tests dir AFTER Stream C copies layer-2/3 over. -3. **`make agent-check` / `make agent-test` not yet run.** Pyright will - currently complain about both `pipelex.runtime_bridge.*` (cache - staleness — files exist) and the still-present old plugin dir. Run - `make cleanderived` first when resuming. +Nothing — all in-repo work is done. Stream D's release / publish steps are +manual and intentional gates, not blockers. ## What to do next, in order -1. **Stream C — write the runtime files in the new repo.** Use the file - contents already in this conversation (or read from - `pipelex/plugins/mistralai_workflows/{activities,streaming,streaming_event_forwarder}.py`) - and write them to `pipelex_mistralai_workflows/`, dropping the optional-dep - guard and rewriting Pipelex imports to `pipelex.runtime_bridge.*`. Add - `pipelex_mistralai_workflows/dependency.py` (task C4) with the - `pipelex_dependency` callable shaped for `mistralai.workflows.Depends(...)`. -2. **Stream C — write the layer-2/3 integration tests + fixtures in the - new repo.** Read sources and write to - `pipelex-mistralai-workflows/tests/integration/{test_*.py,conftest.py,test_data/}` - with rewritten imports per TODOS task C5/C6. The new repo's existing - `tests/integration/conftest.py` (with `check_pipelex_initialized`, - `reset_pipelex_config_fixture`) needs to be merged with a new - `bridge_test_library` fixture pulled from the pipelex conftest. -3. **Stream B finish-up.** - - B1: delete `pipelex_mistralai_workflows/hello_world.py`, - `pipelex_mistralai_workflows/hello_world.mthds`, - `tests/test_pipelines/`, `tests/e2e/test_pipelex_mistralai_workflows.py`. - - B5: rewrite `CHANGELOG.md` (currently still the starter's `[v0.8.0]` - placeholder). - - B6: audit `.github/workflows/tests-check.yml` — install step needs - to also install the `[temporal]` extra so layer-3 tests run. - - B7: audit `Makefile`. Default decision per TODOS: keep as-is. - - B8: `uv lock` + `uv sync --all-extras` and commit `uv.lock`. -4. **Stream A finish-up (in `_workflows/`).** - - A6: delete `pipelex/plugins/mistralai_workflows/` ENTIRELY (only - after Stream C has copied `activities.py` / `streaming.py` / - `streaming_event_forwarder.py` to the new repo). - - A8 finish: delete the now-redundant - `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs - entirely. - - A7: `pyproject.toml` — drop the `mistralai-workflows = [...]` entry - in `[project.optional-dependencies]` (currently line 88) AND the - `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` (currently - lines 154–164). - - A9: delete `docs/under-the-hood/mistralai-workflows-plugin.md` and - `docs/under-the-hood/mistralai-workflows-recipes.md`. Remove the four - `mkdocs.yml` lines (currently at lines 310, 311, 500, 501). - - A10: rewrite the `[Unreleased]` section of `CHANGELOG.md` per - existing TODOS task A10. - - A11: run `make cleanderived && make agent-check && make agent-test`. - Verify the four `git grep` invariants in TODOS A11. +1. **Stream D — coordinated landing & follow-ups.** + - **D1.** Land Stream A's PR on `pipelex` and ship the matching pipelex + release (the one that introduces `pipelex.runtime_bridge` and the + `[Unreleased]` migration paragraph). Same day, push + `pipelex-mistralai-workflows==0.1.0` to PyPI pinning the just-released + `pipelex` minimum. + - **D2 (deferred).** Cookbook entry — defer per §0.6. + - **D3.** Watch the open risks (version coupling, OffloadableField + import drift, CI test parity). + - **D4.** Update root workspace `CLAUDE.md` to add + `pipelex-mistralai-workflows/` to the repo table. +2. **Before publishing v0.1.0**, strip the `[tool.uv.sources]` editable + override from `pipelex-mistralai-workflows/pyproject.toml` so PyPI builds + resolve `pipelex` from PyPI, not the local worktree. Add the override + back at the start of the next dev cycle. ## Open questions / decisions the next session should NOT re-derive -- **Pre-decisions §0.1 / §0.4 are locked.** The framework-agnostic core - lives at `pipelex.runtime_bridge.*`; pipelex-mistralai-workflows pins - `pipelex>=0.27.0`; the editable `[tool.uv.sources]` override is in place. -- **§0.3 is implemented.** New repo version is now `0.1.0`. -- **§0.2 is locked** but the splits in `pipelex_mistralai_workflows/` (the - Mistral-shaped DI helper, etc.) are NOT yet realized in code — that's - Stream C task C4. -- **§0.5 (Mistral component / dependency wrapper shape)** is still - unresolved. Read `mistralai.workflows.plugins.mistralai` before writing - C4. The placeholder is just a callable returning - `Pipelex.get_instance()` after `ensure_pipelex_booted()`. -- **§0.6 (cookbook entry timing).** Deferred — do not block on it. +- **§0.1, §0.3, §0.4 are locked AND implemented.** `pipelex.runtime_bridge` + exists; new repo is `0.1.0`; editable `[tool.uv.sources]` override is in + place and proven to work via `uv sync`. +- **§0.2 is locked AND implemented.** All split assignments are realized in + code. The split is complete; no hidden Mistral-shaped helper remains in + `pipelex.runtime_bridge`. +- **§0.5 is now locked.** The Mistral component / dependency wrapper is the + single function `pipelex_dependency` in + `pipelex_mistralai_workflows/dependency.py` — boots Pipelex, returns the + singleton. Passed to `Depends(pipelex_dependency)`. No `LibraryCrate` + helper added (deferred per §C4 second bullet). +- **§0.6 (cookbook entry timing).** Still deferred — do not block on it. --- @@ -204,57 +215,19 @@ Defaults below are the recommended path. Override only if there's a concrete reason; otherwise proceed. - [x] **0.1 — Framework-agnostic core lives at `pipelex/runtime_bridge/`** - (decision locked). The earlier `pipelex.embedding` proposal was - rejected: "embedding" overlaps too heavily with vector embeddings - and is misleading from the first read. `runtime_bridge` says what - the package actually does — bridge the Pipelex runtime into a host - runtime (Mistral Workflows, raw Temporal, future plugins). -- [ ] **0.2 — Mistral-specific bits stay in the new repo, agnostic bits move - to `pipelex.runtime_bridge`.** Concrete split: - - **Move to `pipelex/runtime_bridge/`:** `bridge.py`, `execution_mode.py`, - `bootstrap.py::ensure_pipelex_booted`, the agnostic exceptions - (`PipelexBridgeRuntimeError`, `MissingPipelexTemporalExtraError`). - - **Move to `pipelex_mistralai_workflows/`:** `activities.py`, - `streaming.py`, `streaming_event_forwarder.py`, - `bootstrap.py::get_pipelex_dependency` (Mistral-shaped — references - `mistralai.workflows.Depends`). - - **Delete entirely:** `MistralWorkflowsPluginError`, - `MistralWorkflowsNotInstalledError`. Once `mistralai-workflows>=3.3.0` - is a hard dep of the new repo, the optional-dep guards in - `activities.py` / `streaming.py` are obsolete and the import-fail - exception goes with them. -- [ ] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** Currently - `0.8.0` (starter inheritance) — that version space is wrong for a - brand-new project. First release ships as `v0.1.0`. + (decision locked). +- [x] **0.2 — Mistral-specific bits stay in the new repo, agnostic bits move + to `pipelex.runtime_bridge`.** Split implemented as planned. +- [x] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** - [x] **0.4 — Pin `pipelex>=0.27.0` in the new repo, plus an editable - `[tool.uv.sources]` override for local dev.** `0.27.0` is the - pipelex release that lands `pipelex.runtime_bridge` (chosen as a - minor bump because the extraction is a breaking change for users - of the old `pipelex.plugins.mistralai_workflows.*` import path). - Bump the minimum on every pipelex release that touches the bridge - surface. Independent SemVer for the plugin pkg. - - Until `v0.1.0` ships, `pipelex-mistralai-workflows` must consume - `pipelex` from this worktree so edits to `pipelex/runtime_bridge/` - are picked up immediately by the plugin's tests: - - ```toml - # ../pipelex-mistralai-workflows/pyproject.toml - [tool.uv.sources] - pipelex = { path = "../_workflows", editable = true } - ``` - - Strip this override before publishing `v0.1.0` — PyPI builds must - resolve `pipelex` from PyPI, not a relative path. (Add the override - back on the next dev cycle when the next breaking change to - `pipelex.runtime_bridge` lands.) -- [ ] **0.5 — Mistral component / dependency wrapper shape.** Read the - Mistral plugin docs first: - `.claude/skills/workflows/references/guides/workflows-plugins.mdx` - and . - Cross-check `references/guides/dependency-injection.mdx` for the - `Depends(...)` shape. Stream C task C4 below holds the - implementation placeholder. + `[tool.uv.sources]` override for local dev.** Implemented. + Reminder: strip the `[tool.uv.sources]` override before publishing + `v0.1.0` so PyPI builds resolve `pipelex` from PyPI, not a relative + path. +- [x] **0.5 — Mistral component / dependency wrapper shape.** Implemented + as a single `pipelex_dependency()` callable in + `pipelex_mistralai_workflows/dependency.py`. Suitable for + `Depends(pipelex_dependency)`. - [ ] **0.6 — Cookbook entry timing.** Defer `pipelex-cookbook/examples/c_advanced/mistral-workflows/` until after `pipelex-mistralai-workflows==0.1.0` is on PyPI (Stream D). @@ -269,181 +242,81 @@ framework-agnostic core lives at `pipelex.runtime_bridge.*`. ### A1. Create the new package -- [x] Create `pipelex/runtime_bridge/` with an empty `__init__.py` (no - re-exports — Pipelex rule). +- [x] Create `pipelex/runtime_bridge/` with an empty `__init__.py`. ### A2. Move `bridge.py` -- [x] Move `pipelex/plugins/mistralai_workflows/bridge.py` → - `pipelex/runtime_bridge/bridge.py`. **(Done as a copy — original - not yet deleted; A6 deletes the source dir.)** -- [x] Rewrite imports inside `bridge.py`: - - `from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted` - → `from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted` - - `from pipelex.plugins.mistralai_workflows.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` - → `from pipelex.runtime_bridge.exceptions import (MissingPipelexTemporalExtraError, PipelexBridgeRuntimeError)` - - `from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode` - → `from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode` -- [x] Rename the per-call library id prefix on line 222: - `f"mistralai_workflows_{uuid4().hex[:8]}"` → - `f"runtime_bridge_{uuid4().hex[:8]}"`. -- [x] Update the install hint in `_require_pipelex_temporal_extra`: - `"pip install 'pipelex[temporal,mistralai-workflows]'"` → - `"pip install 'pipelex[temporal]'"`. -- [x] Update the module docstring: drop "of the mistralai_workflows plugin", - reframe as "framework-agnostic Pipelex runtime-bridge surface for - host runtimes (Mistral Workflows, raw Temporal, future plugins)". +- [x] Move + rewrite imports + rename library-id prefix + update install + hint + reframe docstring. ### A3. Move `execution_mode.py` -- [x] Move `pipelex/plugins/mistralai_workflows/execution_mode.py` → - `pipelex/runtime_bridge/execution_mode.py`. No import changes inside the - file. Docstring slightly reframed away from Mistral-specific wording. - **(Done as a copy — original not yet deleted.)** +- [x] Move; docstring slightly reframed away from Mistral-specific wording. ### A4. Move `bootstrap.py` (split — keep agnostic, drop Mistral-shaped) -- [x] Move `pipelex/plugins/mistralai_workflows/bootstrap.py` → - `pipelex/runtime_bridge/bootstrap.py`. **(Done as a copy — original - not yet deleted.)** -- [x] Keep `ensure_pipelex_booted(...)` verbatim. Update the module - docstring: drop "for use inside Mistral Workflows activities", reframe - as "for use inside any host runtime that embeds Pipelex". -- [x] **Delete** `get_pipelex_dependency()` from `pipelex/runtime_bridge/bootstrap.py` - — it returns a callable explicitly shaped for `mistralai.workflows.Depends` - and belongs in the new repo. Its replacement lives in - `pipelex_mistralai_workflows/dependency.py` (Stream C, task C4). - **(The new file omits the function entirely.)** +- [x] Move; keep `ensure_pipelex_booted`. `get_pipelex_dependency` removed + (lives in the new repo per C4). ### A5. Split `exceptions.py` -- [x] Create `pipelex/runtime_bridge/exceptions.py` with: - - `PipelexRuntimeBridgeError(PipelexError)` — new base (replaces - `MistralWorkflowsPluginError`). - - `MissingPipelexTemporalExtraError(PipelexRuntimeBridgeError)`. - - `PipelexBridgeRuntimeError(PipelexRuntimeBridgeError)`. -- [x] **Do NOT** carry `MistralWorkflowsNotInstalledError` over — it goes - away entirely (the new repo has `mistralai-workflows>=3.3.0` as a - hard dep, so the optional-dep guard pattern is obsolete). +- [x] Created `pipelex/runtime_bridge/exceptions.py` with the new base + `PipelexRuntimeBridgeError` + `MissingPipelexTemporalExtraError` + + `PipelexBridgeRuntimeError`. `MistralWorkflowsNotInstalledError` + intentionally dropped. ### A6. Delete the old plugin directory -- [ ] After A2–A5 are complete and tests still pass, delete the entire - directory `pipelex/plugins/mistralai_workflows/`. This includes: - - `__init__.py` - - `bridge.py` (moved in A2) - - `bootstrap.py` (moved in A4) - - `exceptions.py` (split in A5) - - `execution_mode.py` (moved in A3) - - `activities.py` (deleted; lives in new repo per Stream C) - - `streaming.py` (deleted; lives in new repo per Stream C) - - `streaming_event_forwarder.py` (deleted; lives in new repo per Stream C) +- [x] `pipelex/plugins/mistralai_workflows/` removed. ### A7. Update `pyproject.toml` -- [ ] Remove the `mistralai-workflows = ["mistralai-workflows>=3.3.0"]` - entry from `[project.optional-dependencies]` (currently line 88). -- [ ] Remove the entire `[[tool.mypy.overrides]]` block for - `mistralai.workflows.*` / `mistralai.workflows` (currently lines - 154–164). Pipelex no longer imports anything from that namespace. +- [x] `mistralai-workflows` extra removed. +- [x] `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` removed. ### A8. Move/delete tests -Layer-1 (framework-agnostic) tests follow the runtime-bridge core into pipelex. -Layer-2 / layer-3 tests (which actually instantiate Mistral -`WorkflowEnvironment` / activities) go to the new repo via Stream C. - -- [x] **Move** `tests/unit/pipelex/plugins/mistralai_workflows/` → - `tests/unit/pipelex/runtime_bridge/`: - - `test_input_models.py` - - `test_execution_mode.py` - - `test_validation.py` - - `test_dispatch.py` - - In each, rewrite `pipelex.plugins.mistralai_workflows.*` imports → - `pipelex.runtime_bridge.*`. - **(Done as copies — originals not yet deleted; deletion is the bullet - below.)** -- [x] **Move** the layer-1 integration test: - `tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py` - → `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py`. - Rewrite imports. **(Done as copy — original not yet deleted.)** -- [x] **Move the conftest + test_data with it.** They are needed by the - layer-1 test that stays in pipelex AND will be copied to the new repo - (Stream C, C6) for the layer-2 / layer-3 tests: - - `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` - → `tests/integration/pipelex/runtime_bridge/conftest.py`. Update the import - path inside (`from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs` - → `from tests.integration.pipelex.runtime_bridge.test_data.bridge_funcs`). - - `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` - → `tests/integration/pipelex/runtime_bridge/test_data/` (`bridge_test.mthds` - + `bridge_funcs.py`). **(Domain string `mistralai_workflows_bridge_test` - kept verbatim per default in this section.)** -- [ ] **Delete** the layer-2 / layer-3 integration tests (they move to the - new repo via Stream C — DO NOT delete until Stream C has copied them - over): - - `test_activities_direct.py` - - `test_activities_offloaded.py` - - `test_activities_streaming.py` - - `test_bridge_temporal_blocking.py` - - `test_bridge_temporal_fire_and_forget.py` -- [ ] Delete the now-empty - `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` dirs - (do this together with the layer-2/3 deletion above). +- [x] Layer-1 unit tests moved to `tests/unit/pipelex/runtime_bridge/`. +- [x] Layer-1 integration test moved to + `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py` + with the conftest + test_data. +- [x] Layer-2 / layer-3 integration test files deleted from + `_workflows/` (they live in the new repo per Stream C). +- [x] Old plugin test directories + (`tests/{unit,integration}/pipelex/plugins/mistralai_workflows/`) + deleted entirely. ### A9. Move docs -- [ ] **Delete** `docs/under-the-hood/mistralai-workflows-plugin.md` and - `docs/under-the-hood/mistralai-workflows-recipes.md`. Their content - moves to the new repo's docs (Stream B, B3 README + future docs site). -- [ ] **Update `mkdocs.yml`** — remove four lines: - - line 310: `- under-the-hood/mistralai-workflows-plugin.md: "Mistral Workflows Plugin"` - - line 311: `- under-the-hood/mistralai-workflows-recipes.md: "Mistral Workflows Recipes"` - - line 500: `- Mistral Workflows Plugin: under-the-hood/mistralai-workflows-plugin.md` - - line 501: `- Mistral Workflows Recipes: under-the-hood/mistralai-workflows-recipes.md` -- [ ] **Optional stub.** If we want a discoverable redirect, add a single - short page `docs/under-the-hood/mistralai-workflows.md` containing a - one-paragraph "moved to a separate package" notice with a link to - the new repo. Re-wire `mkdocs.yml` to reference it. Default: skip - the stub — the CHANGELOG migration entry (A10) covers discovery. +- [x] Both `under-the-hood/mistralai-workflows-*.md` deleted. +- [x] Four `mkdocs.yml` lines removed. +- [ ] **Optional stub.** Default decision: skip — no + `under-the-hood/mistralai-workflows.md` redirect page added. ### A10. Update `CHANGELOG.md` -The current `[Unreleased]` section has three entries documenting the plugin -landing. Replace with the migration story. - -- [ ] Remove the three plugin-specific bullets from `[Unreleased]` (the - `pipelex.plugins.mistralai_workflows` activity, the streaming - variant, the per-step streaming additions). They will live in the - new repo's CHANGELOG (Stream B, B5). -- [ ] Add a new `[Unreleased]` bullet: - - > **Mistral Workflows integration extracted into a dedicated package.** - > The optional `pipelex[mistralai-workflows]` extra and the - > `pipelex.plugins.mistralai_workflows.*` modules have been removed - > from `pipelex`. Install the new package instead: - > `pip install pipelex-mistralai-workflows`, and import from - > `pipelex_mistralai_workflows.*`. The framework-agnostic runtime-bridge - > core (boundary types, `run_pipe_via_bridge`, `PipelexExecutionMode`, - > `ensure_pipelex_booted`) has been promoted from - > `pipelex.plugins.mistralai_workflows.*` to `pipelex.runtime_bridge.*` so - > any host runtime — not just Mistral Workflows — can embed Pipelex. - > No behavior changes; activities, boundary types, and execution - > modes are identical. - - Per project rule (CLAUDE.md "No backward compatibility"), no compat - shim. The pipelex release that drops the extra ships together with - `pipelex-mistralai-workflows==0.1.0`. +- [x] The three plugin-landing bullets removed from `[Unreleased]`. +- [x] Migration `Changed` bullet added under `[Unreleased]`. ### A11. Verify -- [ ] `make agent-check` clean. -- [ ] `make agent-test` green. -- [ ] `git grep mistralai_workflows pipelex/ tests/ pyproject.toml` returns no hits. -- [ ] `git grep mistralai-workflows pipelex/ tests/ pyproject.toml` returns - only the migration paragraph in `CHANGELOG.md` and the install hint - in `_require_pipelex_temporal_extra` (now removed per A2 — verify). -- [ ] `git grep "pipelex.runtime_bridge" pipelex/ tests/` finds the new package - paths. +- [x] `make cleanderived && make rtm && make agent-check` clean. (`make rtm` + regenerates `_generated_model_sets.py` which `cleanderived` deletes — + pyright fails without it.) +- [x] `make agent-test` green. +- [x] `git grep mistralai_workflows pipelex/ tests/ pyproject.toml` → + remaining hits are all in `tests/integration/pipelex/runtime_bridge/` + test data (domain string `mistralai_workflows_bridge_test`, function + name `mistralai_workflows_bridge_echo`). Per A8's + "minimize churn" decision, these were intentionally kept verbatim; + no production-code reference to the old plugin namespace remains. +- [x] `git grep mistralai-workflows pipelex/ tests/ pyproject.toml` → one + hit, a docstring comment in `test_bridge_direct.py` referring to the + *new* package `pipelex-mistralai-workflows`. Acceptable. +- [x] `git grep mistralai-workflows CHANGELOG.md` → exactly the migration + paragraph. +- [x] `git grep "pipelex.runtime_bridge" pipelex/ tests/` finds 8 files in + the new layout. ### A12. (Out-of-scope reminder) Verify "make agent-check passes without optional dep" @@ -456,330 +329,129 @@ becomes trivially true once A6 + A7 are done — `pipelex` no longer imports ## Stream B — Adapt the `pipelex-mistralai-workflows` scaffold -Currently the repo at `../pipelex-mistralai-workflows/` is the -`pipelex-starter-python` scaffold with a `hello_world` example. Convert -to a library distribution. - ### B1. Strip starter content -- [ ] Delete `pipelex_mistralai_workflows/hello_world.py`. -- [ ] Delete `pipelex_mistralai_workflows/hello_world.mthds`. -- [ ] Keep `pipelex_mistralai_workflows/__init__.py` (empty) and - `pipelex_mistralai_workflows/py.typed`. -- [ ] Delete `tests/test_pipelines/` (starter artifact — no test pipelines - yet) and `tests/e2e/test_pipelex_mistralai_workflows.py` (starter - smoke test that imports `hello_world`). Layer-1+ tests come from - Stream C. +- [x] All starter files removed. +- [x] Empty `tests/e2e/` directory + conftest left in place (harmless). ### B2. Rewrite `pyproject.toml` -- [x] `version = "0.1.0"` (currently `0.8.0`). -- [x] `description = "Mistral Workflows plugin for Pipelex — invoke Pipelex pipes from inside Mistral Workflows activities."` -- [x] `authors = [{ name = "Evotis S.A.S.", email = "oss@pipelex.com" }]`. -- [x] Update `[project.urls]`: - - `Homepage = "https://pipelex.com"` - - `Repository = "https://github.com/Pipelex/pipelex-mistralai-workflows"` - - `Documentation = "https://docs.pipelex.com/"` -- [x] Replace `dependencies` with the slim library shape (`pipelex>=0.27.0` - + `mistralai-workflows>=3.3.0`). No inference / cloud extras. -- [x] Add the `[temporal]` optional extra - (`pipelex[temporal]>=0.27.0`). -- [x] Add the PEP 695 mypy override for `mistralai.workflows.*` / - `mistralai.workflows`. -- [x] Add `pytest-asyncio>=0.24.0`, `pytest-mock>=3.14.0` to the `dev` - extra. -- [x] **Pytest markers** — keep only `gha_disabled`, `dry_runnable`, - `temporal: tests that require a Temporal server`. Dropped - `inference` / `llm` / `img_gen` / `extract` / `pipelex_api` / - `needs_output` / `codex_disabled`. -- [x] `requires-python = ">=3.12,<3.15"` (kept from scaffold — Mistral - Workflows is the binding floor). -- [x] `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }` - — temporary editable override per §0.4. Strip before publishing - v0.1.0. +- [x] All bullets implemented (see snapshot above). +- [x] Added `pythonpath = ["tests"]` under `[tool.pytest]` after a runtime + `ModuleNotFoundError: No module named 'integration'` was hit during + the first `make agent-test` attempt. Project rule forbids + `tests/__init__.py`, so the conftest uses + `from integration.test_data.bridge_funcs import ...` and pytest's + `pythonpath` adds `tests/` to `sys.path` at collection time. ### B3. Replace the README -- [x] Replace `README.md` (currently the starter's) with a library-style - README. Sections: - - Title + one-paragraph pitch ("Invoke Pipelex pipes from inside Mistral - Workflows activities"). - - Install: `pip install pipelex-mistralai-workflows`. Optional Temporal - layer: `pip install 'pipelex-mistralai-workflows[temporal]'`. - - Quick start (Tier 1): import `pipelex_run_pipe`, register on a worker, - call from a workflow. - - Per-call library scoping (Tier 2/3) using `library_crate_dump`. - - Streaming variant (`pipelex_run_pipe_streaming`). - - Migration note (mirror the CHANGELOG entry from Stream A, A10). - - Links: Pipelex docs, MTHDS standard, Mistral Workflows docs. - - Move the bulk of the deleted pipelex docs (`mistralai-workflows-plugin.md` - + `mistralai-workflows-recipes.md`) into the README — the docs site can - come later. Keep the README scannable; deeper recipes can become a - `docs/` subdirectory in a follow-up. +- [x] Replaced. ### B4. Replace `CLAUDE.md` -- [x] Replace with a short repo-specific CLAUDE.md: - - Point at workspace `CLAUDE.md` for global rules. - - Note: do NOT depend on internal `pipelex` paths (e.g. anything under - `pipelex.pipe_run`, `pipelex.libraries`, etc.). Only depend on the - public `pipelex.runtime_bridge.*` surface. - - List the same `make agent-check` / `make agent-test` / `cleanderived` - workflow used in pipelex. - - Mirror pipelex's "No backward compatibility" rule. +- [x] Replaced. ### B5. Rewrite `CHANGELOG.md` -- [ ] Replace existing `[v0.8.0]` placeholder. New top-of-file: - - ```markdown - # Changelog - - ## [Unreleased] - - ## [v0.1.0] - - - First release. Extracts the Mistral Workflows ↔ Pipelex bridge from - `pipelex[mistralai-workflows]` into a dedicated package. - - ### Added - - - `pipelex_mistralai_workflows.activities.pipelex_run_pipe` — pre-decorated - Mistral Workflows activity wrapping `pipelex.runtime_bridge.run_pipe_via_bridge`. - - `pipelex_mistralai_workflows.activities.pipelex_run_pipe_offloaded` — - offload-capable variant for payloads that exceed Temporal's per-event - size limit. - - `pipelex_mistralai_workflows.streaming.pipelex_run_pipe_streaming` — - streaming activity that wraps the run in a Mistral `Task` - (`custom_task_type="pipelex.pipe_run"`) so subscribers see - `CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / - `CustomTaskFailed` events. Emits per-step `CustomTaskInProgress` - events for `DIRECT` execution mode. - - `pipelex_mistralai_workflows.dependency.pipelex_dependency` — Mistral - component / dependency wrapper around `ensure_pipelex_booted` (see - §0.5 — final shape TBD pending a read of - `mistralai.workflows.plugins.mistralai`). - - ### Changed - - - Migrated from `pipelex.plugins.mistralai_workflows.*` to - `pipelex_mistralai_workflows.*`. Framework-agnostic types - (`PipelexPipeRunInput`, `PipelexPipeRunOutput`, - `run_pipe_via_bridge`, `PipelexExecutionMode`, - `ensure_pipelex_booted`) now imported from `pipelex.runtime_bridge.*`. - ``` - - Carry the three original landing-narrative bullets from the old - pipelex `[Unreleased]` (deleted in A10) into the **Added** section - above, rewriting the import paths to the new namespaces. +- [x] Replaced. Carried the three landing bullets into `[v0.1.0]` Added, + added the dependency-helper bullet, and a Changed bullet for the + namespace migration. ### B6. Audit `.github/workflows/` -The starter shipped 8 workflows. Verify each is fit-for-purpose: - -- [ ] `tests-check.yml` — the test job needs to install `pipelex[temporal]` - via the new `[temporal]` extra so layer-3 tests run. Confirm the - install step uses `uv sync --extra temporal` (or equivalent). -- [ ] `lint-check.yml` — should already work (calls `make` targets). -- [ ] `package-check.yml` — verifies the wheel builds; no changes. -- [ ] `version-check.yml` — verifies version bumps follow SemVer; review - that it works for a non-app library. -- [ ] `changelog-check.yml` — verifies CHANGELOG was updated on PRs; - confirm format expected matches B5. -- [ ] `cla.yml`, `guard-branches.yml`, `github-release.yml` — generic; keep - as-is, confirm they reference the right repo. - -If any workflow assumes starter conventions that don't apply, prune. +- [x] Reviewed. `tests-check.yml` already runs `make install` → + `uv sync --all-extras`, which installs the `[temporal]` extra. No + edits needed. The other 7 workflows (lint, package, version, + changelog, cla, guard-branches, github-release) are generic and + reference the right repo. ### B7. Audit `Makefile` -- [ ] Confirm all targets resolve in the new dep layout. Specifically: - - `make agent-check` should work without `mistralai-workflows`-specific - knowledge (it's a hard dep now). - - `make validate` calls `pipelex validate --all` — works only if the - package directory contains valid `.mthds` (currently the starter's - `hello_world.mthds` is being deleted in B1; layer-2 tests carry their - own `bridge_test.mthds` under `tests/integration/test_data/`). Decide - whether `make validate` is meaningful for this repo. Default: keep the - target; it's a no-op when there are no `.mthds` in the package. +- [x] Default decision honored: keep `make validate` as-is (it's a no-op + when there are no `.mthds` in `pipelex_mistralai_workflows/`). ### B8. Refresh `uv.lock` -- [ ] After B2 lands, run inside the new repo: - - ```bash - uv lock - uv sync --all-extras - ``` - - Commit the updated `uv.lock`. +- [x] `uv lock` + `uv sync --all-extras` ran cleanly; the editable + `pipelex` install resolves to the worktree path. + `mistralai-workflows` is locked at `==3.4.0` (the floor is + `>=3.3.0`; both 3.3.0 and 3.4.0 confirmed working once the + `deployment_name` fixture override is in place — see C7). + (Lock file is uncommitted in the new repo's working tree until you + commit.) --- ## Stream C — Move plugin code into the new repo -Coordinated with the pipelex deletions in Stream A. Land Stream A's PR and -Stream C's first commit in lockstep so `git bisect` always builds. - ### C1. Move `activities.py` -- [ ] Move `pipelex/plugins/mistralai_workflows/activities.py` → - `pipelex_mistralai_workflows/activities.py`. -- [ ] **Drop the optional-dep guard.** Replace: - - ```python - try: - from mistralai.workflows import activity - from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel - except ImportError as exc: - msg = (...) - raise MistralWorkflowsNotInstalledError(msg) from exc - ``` - - with bare imports: - - ```python - from mistralai.workflows import activity - from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel - ``` -- [ ] Rewrite Pipelex imports: - - `from pipelex.plugins.mistralai_workflows.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` - → `from pipelex.runtime_bridge.bridge import (PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge)` - - Drop the `from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError` import (exception deleted). +- [x] Moved + guard dropped + imports rewritten. ### C2. Move `streaming.py` -- [ ] Move `pipelex/plugins/mistralai_workflows/streaming.py` → - `pipelex_mistralai_workflows/streaming.py`. -- [ ] Drop the optional-dep guard (same pattern as C1). -- [ ] Rewrite imports: - - `pipelex.plugins.mistralai_workflows.bridge` → `pipelex.runtime_bridge.bridge` - - `pipelex.plugins.mistralai_workflows.execution_mode` → `pipelex.runtime_bridge.execution_mode` - - `pipelex.plugins.mistralai_workflows.streaming_event_forwarder` → `pipelex_mistralai_workflows.streaming_event_forwarder` - - Drop the `MistralWorkflowsNotInstalledError` import. +- [x] Moved + guard dropped + imports rewritten. ### C3. Move `streaming_event_forwarder.py` -- [ ] Move `pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py` - → `pipelex_mistralai_workflows/streaming_event_forwarder.py`. The - file has no `mistralai.workflows` imports and no - `pipelex.plugins.mistralai_workflows` imports — it's already - framework-agnostic. No edits needed beyond placement. -- [ ] Optionally: keep the writer_id `"mistralai-workflows-streaming"` - verbatim — it's a stable identifier that downstream observers may - already key off of. +- [x] Copied verbatim. +- [x] writer_id `"mistralai-workflows-streaming"` kept verbatim. ### C4. Add the Mistral component / dependency wrapper -- [ ] Create `pipelex_mistralai_workflows/dependency.py`. Before writing - it, **read** `mistralai/workflows/plugins/mistralai` (the - reference plugin) and mirror its dependency-component shape. -- [ ] Provide at minimum: - - `pipelex_dependency` — a callable shaped for - `mistralai.workflows.Depends(...)`. Body wraps - `ensure_pipelex_booted()` (imported from `pipelex.runtime_bridge.bootstrap`) - and returns `Pipelex.get_instance()`. This is the function previously - living as `get_pipelex_dependency()` in - `pipelex.plugins.mistralai_workflows.bootstrap` (deleted in Stream A, - A4) — port it over with the Mistral-specific docstring. -- [ ] Optional: a `LibraryCrate` snapshot helper exposing - `library_crate_dump` per-call without forcing every caller to - hand-roll the `LibraryCrate.model_dump(...)` call. Defer if the - reference plugin doesn't follow this pattern. +- [x] `pipelex_mistralai_workflows/dependency.py` written. Single + `pipelex_dependency()` callable shaped for + `mistralai.workflows.Depends(...)`. +- [ ] Optional `LibraryCrate` snapshot helper — deferred. Reference + Mistral plugin (`mistralai.workflows.plugins.mistralai`) does not + mandate it; revisit after first user feedback. ### C5. Move integration tests (layer-2 / layer-3) -For each file, move from -`_workflows/tests/integration/pipelex/plugins/mistralai_workflows/` -to `pipelex-mistralai-workflows/tests/integration/`. - -- [ ] `test_activities_direct.py` -- [ ] `test_activities_offloaded.py` -- [ ] `test_activities_streaming.py` -- [ ] `test_bridge_temporal_blocking.py` -- [ ] `test_bridge_temporal_fire_and_forget.py` - -For each, rewrite imports: - -- `from pipelex.plugins.mistralai_workflows.bridge import ...` - → `from pipelex.runtime_bridge.bridge import ...` -- `from pipelex.plugins.mistralai_workflows.execution_mode import ...` - → `from pipelex.runtime_bridge.execution_mode import ...` -- `from pipelex.plugins.mistralai_workflows.activities import ...` - → `from pipelex_mistralai_workflows.activities import ...` -- `from pipelex.plugins.mistralai_workflows.streaming import ...` - → `from pipelex_mistralai_workflows.streaming import ...` -- `from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs import ...` - → `from tests.integration.test_data.bridge_funcs import ...` +- [x] All five files moved with imports rewritten: + - `test_activities_direct.py` + - `test_activities_offloaded.py` + - `test_activities_streaming.py` + - `test_bridge_temporal_blocking.py` + - `test_bridge_temporal_fire_and_forget.py` ### C6. Move test fixtures -- [ ] Copy - `tests/integration/pipelex/plugins/mistralai_workflows/conftest.py` → - `pipelex-mistralai-workflows/tests/integration/conftest.py`. The new - conftest needs to **merge** with the existing scaffold conftest - (which has `check_pipelex_initialized` and - `reset_pipelex_config_fixture`). Strategy: - - Keep the scaffold's `check_pipelex_initialized` and - `reset_pipelex_config_fixture` (session/module-scoped Pipelex setup). - - Add `bridge_test_library` (class-scoped) from the pipelex conftest. - - Update its import: `from tests.integration.test_data.bridge_funcs import ...`. -- [ ] Copy - `tests/integration/pipelex/plugins/mistralai_workflows/test_data/` - → `pipelex-mistralai-workflows/tests/integration/test_data/`: - - `bridge_test.mthds` - - `bridge_funcs.py` - - Note: the same files also live in pipelex at - `tests/integration/pipelex/runtime_bridge/test_data/` (per Stream A, A8) for - the layer-1 bridge test. This is intentional duplication: both repos - exercise the same fixture against different layers. If divergence - becomes a maintenance problem later, factor into a tiny shared package; - for v0.1.0 keep duplicated. +- [x] `tests/integration/conftest.py` merged with the scaffold's existing + fixtures plus the `bridge_test_library` class-scoped fixture. +- [x] `tests/integration/test_data/{bridge_test.mthds,bridge_funcs.py}` + copied across. ### C7. Verify the new repo -- [ ] In `../pipelex-mistralai-workflows/`: - - ```bash - make install - make agent-check - make agent-test - ``` -- [ ] Run the layer-3 (Temporal) tests explicitly with the `temporal` extra: - - ```bash - .venv/bin/uv sync --extra temporal --extra dev - .venv/bin/pytest tests/integration/test_bridge_temporal_blocking.py \ - tests/integration/test_bridge_temporal_fire_and_forget.py - ``` -- [ ] Run the streaming layer-2 test with detailed logging to verify the - per-step `CustomTaskInProgress` event flow still asserts correctly: - - ```bash - .venv/bin/pytest -s tests/integration/test_activities_streaming.py - ``` -- [ ] Smoke import in a fresh shell: +- [x] `make agent-check` clean. +- [x] `make agent-test` green — all 8 tests pass (3 layer-2 activity + tests, 2 layer-3 Temporal tests, 2 fundamentals, 1 dry-run-all). + **Required test fixture fix**: in all 3 layer-2 test files, the + `override_mistralai_task_queue` fixture also clears + `mistralai_config.worker.deployment_name = None`. Without this, a + developer `.env` with `DEPLOYMENT_NAME=...` (or any non-test value) + causes Mistral's `get_effective_task_queue()` to route activities + to that deployment name instead of `TEST_TASK_QUEUE`, leading to a + silent workflow hang. +- [x] Smoke imports already validated: ```python from pipelex_mistralai_workflows.activities import pipelex_run_pipe, pipelex_run_pipe_offloaded from pipelex_mistralai_workflows.streaming import pipelex_run_pipe_streaming + from pipelex_mistralai_workflows.dependency import pipelex_dependency from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted ``` - All six imports succeed without warnings. ### C8. First release - [ ] Tag `v0.1.0` in `pipelex-mistralai-workflows`. -- [ ] Push the tag and create the GitHub release (use `release` skill if - available in the new repo, else manual). +- [ ] Push the tag and create the GitHub release. - [ ] Publish to PyPI as `pipelex-mistralai-workflows==0.1.0`. -- [ ] Coordinate timing: this PyPI release ships **together with** the - pipelex release that drops the `[mistralai-workflows]` extra (Stream - A, A11 / migration paragraph in CHANGELOG). +- [ ] Coordinate with the matching pipelex release (Stream D §D1). --- @@ -806,9 +478,9 @@ For each, rewrite imports: ### D3. Watch the open risks -- [ ] **Version coupling.** Document the `pipelex.runtime_bridge` public surface - as stable in pipelex docs. A breaking change to that surface is a - breaking change for the plugin pkg. +- [ ] **Version coupling.** Document the `pipelex.runtime_bridge` public + surface as stable in pipelex docs. A breaking change to that surface + is a breaking change for the plugin pkg. - [ ] **OffloadableField import drift.** `activities.py` (now in the new repo) imports `OffloadableField, OffloadableModel` from `mistralai.workflows.core.encoding.fields_offloader`. If a Mistral @@ -834,10 +506,10 @@ If you're picking this up cold: design decisions and gotchas. Treat as spec; don't re-derive. 2. Read `wip/mistral-workflows-plugin-extract.md` end-to-end — the strategy. This file (`TODOS.md`) is the execution layer. -3. Resolve §0 pre-decisions if not already locked. Defaults are usable. -4. Pick a stream: - - Streams A and B are independent — run in parallel. - - Stream C waits on both A and B. - - Stream D waits on C. +3. §0 pre-decisions are locked except §0.6 (cookbook timing — still + deferred). Do not re-debate. +4. Pick up from §Progress snapshot's "What to do next, in order" — the + pending items are A11 verification + the in-flight new-repo + `make agent-test` outcome + Stream D landing. 5. After every step: `make agent-check && make agent-test` in whichever repo you touched. diff --git a/docs/under-the-hood/mistralai-workflows-plugin.md b/docs/under-the-hood/mistralai-workflows-plugin.md deleted file mode 100644 index 5890bc115..000000000 --- a/docs/under-the-hood/mistralai-workflows-plugin.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: "Mistral Workflows Plugin" -description: "Run Pipelex pipes from inside Mistral Workflows activities — install, execution modes, and when to pick which." ---- - -# Mistral Workflows Plugin - -The `pipelex.plugins.mistralai_workflows` plugin lets you call Pipelex pipes from inside [Mistral Workflows](https://docs.mistral.ai/) activities. Pipelex remains in charge of pipe orchestration; Mistral Workflows owns the surrounding activity, retry policy, scheduling, and (optionally) durable execution. - -For worked examples (Tier 1 pre-decorated activity, Tier 2 helper-in-your-own-activity, Tier 3 full control with `library_crate_dump`), see the [Recipes](./mistralai-workflows-recipes.md) page. - ---- - -## Install - -The `mistralai-workflows` dependency is **strictly optional**. Install it as an extra: - -```bash -pip install 'pipelex[mistralai-workflows]' -``` - -For the `TEMPORAL_BLOCKING` and `TEMPORAL_FIRE_AND_FORGET` execution modes, also install the `temporal` extra: - -```bash -pip install 'pipelex[mistralai-workflows,temporal]' -``` - -The framework-agnostic core (`bridge.py`, `execution_mode.py`, `bootstrap.py`, `exceptions.py`) is importable on a venv that does NOT have `mistralai-workflows` installed. The optional-dep guard fires only when you import `pipelex.plugins.mistralai_workflows.activities` or `pipelex.plugins.mistralai_workflows.streaming`. - ---- - -## What you can import - -Per Pipelex's no-re-exports rule, import from the full path: - -```python -from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.plugins.mistralai_workflows.bootstrap import ( - ensure_pipelex_booted, - get_pipelex_dependency, -) -from pipelex.plugins.mistralai_workflows.streaming import pipelex_run_pipe_streaming -``` - ---- - -## Execution modes - -`PipelexExecutionMode` is set per-call via `PipelexPipeRunInput.execution_mode`. It is exhaustive: any new mode added later will surface as a linting error in every `match` statement that consumes it. - -### `DIRECT` - -The pipe runs in-process inside the Mistral activity. No Temporal involvement on Pipelex's side. The activity blocks until the pipe completes. - -- **When to use:** simple integrations, fast feedback, tests, environments without a Pipelex Temporal worker. -- **Requires:** `pipelex[mistralai-workflows]`. - -### `TEMPORAL_BLOCKING` - -The bridge dispatches the pipe as a Pipelex Temporal workflow (`WfPipeRun`) and awaits completion. The pipe runs durably on the Pipelex worker fleet; the Mistral activity blocks until that workflow returns. - -- **When to use:** you already operate a Pipelex Temporal cluster and want pipes to run durably with Pipelex's existing observability and retry semantics. -- **Requires:** `pipelex[mistralai-workflows,temporal]`. - -### `TEMPORAL_FIRE_AND_FORGET` - -The bridge dispatches the pipe as a Pipelex Temporal workflow and returns immediately with the workflow id. The activity does NOT wait. Completion is delivered out-of-band via a `DeliveryAssignment` (storage and/or webhook). - -- **When to use:** long-running pipes (multi-minute LLM jobs, large extractions) where you don't want the surrounding Mistral activity to keep its slot for the full duration. -- **Requires:** `pipelex[mistralai-workflows,temporal]`. -- **Validation:** `delivery_assignment_dump` must be set; otherwise `run_pipe_via_bridge` raises `PipelexBridgeRuntimeError` to prevent silently-dropped completions. - ---- - -## Boundary types - -Everything that crosses the Mistral/Temporal boundary is JSON-only: - -- `PipelexPipeRunInput.inputs` — `dict[str, Any]` -- `PipelexPipeRunInput.library_crate_dump` — `dict[str, Any] | None` (a `LibraryCrate.model_dump(mode="json")`) -- `PipelexPipeRunInput.delivery_assignment_dump` — `dict[str, Any] | None` (a `DeliveryAssignment.model_dump(mode="json")`) -- `PipelexPipeRunOutput.output_dict` — `dict[str, Any]` produced by `WorkingMemory.dump_for_temporal()` -- `PipelexPipeRunOutput.graph_spec_dump` — `dict[str, Any] | None` - -No internal Pipelex types (`PipeJob`, `PipeOutput`, `WorkingMemory`) cross the activity boundary. The bridge serializes via `WorkingMemory.dump_for_temporal()` regardless of execution mode, so the `output_dict` shape is stable. - ---- - -## Bootstrapping - -Boot Pipelex once before the Mistral worker starts; the activity is then a thin wrapper around `run_pipe_via_bridge`: - -```python -import asyncio -from mistralai import workflows - -from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe -from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted - - -async def main() -> None: - ensure_pipelex_booted() - await workflows.run_worker([MyFlow], activities=[pipelex_run_pipe]) - - -asyncio.run(main()) -``` - -`ensure_pipelex_booted()` is idempotent and safe to call from inside the activity too — useful for tests or first-run safety nets — but in production it should be called explicitly at worker startup so Pipelex initialization is not on the critical path of the first activity. - ---- - -## Per-call library scoping (`library_crate_dump`) - -When a `library_crate_dump` is provided on the input, the bridge opens a per-call scoped library, loads the crate, runs the pipe inside that scope, and tears down on the way out. The global registry is left untouched. - -This is the same scoping mechanism Pipelex's own Temporal layer uses (`pipelex/temporal/tprl_pipe/wf_pipe_router.py`) and is the recommended way to invoke a pipe whose bundle is not pre-loaded into the worker's global registry — for example, when the calling activity received the bundle as part of an API request. - ---- - -## Error mapping - -The bridge maps Pipelex execution errors into a single `PipelexBridgeRuntimeError` chained from the original exception. Mistral / Temporal infrastructure errors (connection, dispatch) propagate unchanged. - -| Exception | When | -| -------------------------------------- | ----------------------------------------------------------------------------- | -| `MistralWorkflowsNotInstalledError` | Importing `activities` (or `streaming`) without the optional dep installed | -| `MissingPipelexTemporalExtraError` | Calling `TEMPORAL_*` modes without `pipelex[temporal]` installed | -| `PipelexBridgeRuntimeError` | Pipe execution failed; original exception is on `__cause__` | -| `MistralWorkflowsPluginError` | Common base for plugin-specific errors | - ---- - -## Boundary semantics summary - -| Aspect | DIRECT | TEMPORAL_BLOCKING | TEMPORAL_FIRE_AND_FORGET | -| ----------------------- | --------------------- | -------------------------------- | ------------------------------------ | -| Pipelex worker required | No | Yes | Yes | -| Activity blocks | Yes | Yes (until WfPipeRun completes) | No (returns workflow_id immediately) | -| `is_completed` returned | `True` | `True` | `False` | -| `workflow_id` returned | `None` | Pipelex Temporal workflow id | Pipelex Temporal workflow id | -| `output_dict` populated | Yes | Yes | `{}` | -| Completion delivery | In-band | In-band | Out-of-band via `DeliveryAssignment` | diff --git a/docs/under-the-hood/mistralai-workflows-recipes.md b/docs/under-the-hood/mistralai-workflows-recipes.md deleted file mode 100644 index d388bc30a..000000000 --- a/docs/under-the-hood/mistralai-workflows-recipes.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -title: "Mistral Workflows Recipes" -description: "Three integration tiers for invoking Pipelex pipes from Mistral Workflows activities — pre-decorated, helper-in-your-own-activity, full control." ---- - -# Mistral Workflows Recipes - -For the architecture and execution-mode reference, see the [plugin overview](./mistralai-workflows-plugin.md). - -The plugin offers three usage tiers, in order of decreasing convenience and increasing control. Pick the tier that matches how much customization you need around the activity itself. - ---- - -## Tier 1 — pre-decorated activity (the fast path) - -Use the ready-made `pipelex_run_pipe` activity directly. Nothing to configure beyond pipe code and inputs. - -```python -import asyncio -from mistralai import workflows - -from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe -from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - - -@workflows.workflow.define(name="extract-invoice-flow") -class ExtractInvoiceFlow: - @workflows.workflow.entrypoint - async def run(self, doc_url: str) -> dict: - result = await pipelex_run_pipe( - PipelexPipeRunInput( - pipe_code="finance.extract_invoice", - inputs={"doc_url": doc_url}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - return result.output_dict - - -async def main() -> None: - ensure_pipelex_booted() - await workflows.run_worker([ExtractInvoiceFlow], activities=[pipelex_run_pipe]) - - -asyncio.run(main()) -``` - -The activity has sensible defaults (10 minute timeout, 3 retries). When you need different timeouts, retry policies, rate limits, or sticky-to-worker behavior — go to Tier 2. - ---- - -## Large payloads — `pipelex_run_pipe_offloaded` - -Temporal's per-event payload limit is around 2 MiB. When a pipe input or output approaches that ceiling — large documents, accumulated transcripts, image bytes — the activity rejects with `MessageTooLarge`. Mistral Workflows ships an `ActivityInOutOffloadingInterceptor` that streams oversized payloads through blob storage (S3/GCS/Azure) automatically, and Pipelex provides an offload-capable activity to plug into it. - -```python -from mistralai import workflows -from mistralai.workflows.core.encoding.fields_offloader import OffloadableField - -from pipelex.plugins.mistralai_workflows.activities import ( - PipelexPipeRunInputOffloaded, - PipelexPipeRunOutputOffloaded, - pipelex_run_pipe_offloaded, -) -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput - - -@workflows.workflow.define(name="extract-large-doc-flow") -class ExtractLargeDocFlow: - @workflows.workflow.entrypoint - async def run(self, doc_bytes: bytes) -> dict: - wrapped_input = PipelexPipeRunInputOffloaded( - payload=OffloadableField( - value=PipelexPipeRunInput( - pipe_code="finance.extract_large_invoice", - inputs={"doc_bytes": doc_bytes.hex()}, - ), - ), - ) - wrapped_output: PipelexPipeRunOutputOffloaded = await pipelex_run_pipe_offloaded(wrapped_input) - return wrapped_output.payload.get_value().output_dict -``` - -The wrapping/unwrapping is a no-op when the payload fits inline. Offloading only kicks in when the worker is configured with the interceptor: - -```python -from mistralai import workflows -from mistralai.workflows.core.config.config import config -from mistralai.workflows.core.encoding.fields_offloader import FieldsOffloader -from mistralai.workflows.core.temporal.activity_offloading_interceptor import ( - ActivityInOutOffloadingInterceptor, -) - -offloader = FieldsOffloader(offloading_config=config.payload_offloading) -interceptor = ActivityInOutOffloadingInterceptor(offloader) - -await workflows.run_worker( - [ExtractLargeDocFlow], - activities=[pipelex_run_pipe_offloaded], - interceptors=[interceptor], -) -``` - -Trade-off: offloaded payloads live in the blob storage you configure (S3 by default in Mistral's example) for the lifetime of the workflow run. They incur storage cost and add an extra round-trip per offloaded field. Reach for the offloaded variant only when you actually need the size headroom. - ---- - -## Live progress events — `pipelex_run_pipe_streaming` - -When a UI subscribes to a Mistral Workflow execution and needs to "see something happen" while a Pipelex pipe runs, use the streaming variant. It wraps the same bridge call in a single Mistral `Task` whose lifecycle (`CustomTaskStarted` → `CustomTaskInProgress` → `CustomTaskCompleted` / `CustomTaskFailed`) is published to whatever events client your worker is configured with. - -```python -from mistralai import workflows - -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, -) -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.plugins.mistralai_workflows.streaming import pipelex_run_pipe_streaming - - -@workflows.workflow.define(name="extract-invoice-streaming-flow") -class ExtractInvoiceStreamingFlow: - @workflows.workflow.entrypoint - async def run(self, doc_url: str) -> dict: - result: PipelexPipeRunOutput = await pipelex_run_pipe_streaming( - PipelexPipeRunInput( - pipe_code="finance.extract_invoice", - inputs={"doc_url": doc_url}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - return result.output_dict - - -await workflows.run_worker( - [ExtractInvoiceStreamingFlow], - activities=[pipelex_run_pipe_streaming], -) -``` - -The events carry a small JSON payload identifying the run: - -| Event | Payload | -| ------------------------ | ----------------------------------------------------------------------------- | -| `CustomTaskStarted` | `phase="started"`, `pipe_code`, `execution_mode`, `pipeline_run_id` (if set) | -| `CustomTaskInProgress` | JSON-patch updates: per-step boundaries (DIRECT mode) and the final transition to `phase="completed"` | -| `CustomTaskCompleted` | Final full-state snapshot with `phase="completed"` and `main_stuff_name` | -| `CustomTaskFailed` | The original exception message (emitted by `Task.__aexit__` on failure) | - -`custom_task_type` is always `"pipelex.pipe_run"`, so subscribers can filter on it without parsing the payload. - -### Per-step events for `DIRECT` mode - -When `execution_mode=PipelexExecutionMode.DIRECT`, the streaming activity publishes one `CustomTaskInProgress` event per Pipelex pipe boundary in addition to the final completed-state push. Each pipe-step event carries a JSON-patch update to the streaming state with the following fields: - -| Field | Description | -| --------------------------- | ---------------------------------------------------------------------------- | -| `phase` | `"in_progress"` (transition from `"started"` on the very first patch) | -| `current_step_pipe_code` | The pipe code for the most recent `PipeStartEvent` | -| `current_step_node_id` | The graph node id for that pipe | -| `last_event_kind` | `"pipe_start"` / `"pipe_end_success"` / `"pipe_end_error"` | -| `started_steps` | Cumulative count of pipe-step starts (1-indexed, monotonic) | -| `completed_steps` | Cumulative count of successful pipe-step completions | -| `last_output_stuff_name` | The output IOSpec name for the most recent successful step (or `null`) | - -A field only appears in a given `CustomTaskInProgress` JSON-patch when its value actually changed — for example, `last_event_kind` won't appear in two consecutive `pipe_start` events. Use `started_steps` / `completed_steps` (always changing) as discriminators when you need to count or order step events. - -`TEMPORAL_BLOCKING` and `TEMPORAL_FIRE_AND_FORGET` modes keep the simpler "one started + one completed" semantics — per-step streaming across the Temporal worker boundary is not supported in this release. - -For the silent path (no observability, no event publishing overhead per activity) keep using `pipelex_run_pipe` — the streaming variant is opt-in. - ---- - -## Tier 2 — helper inside your own typed activity - -Wrap `run_pipe_via_bridge` in your own `@activity`-decorated function so you control all activity options and the input/output types. - -```python -from datetime import timedelta - -from mistralai import workflows -from pydantic import BaseModel - -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - run_pipe_via_bridge, -) - - -class InvoiceData(BaseModel): - invoice_number: str - total_amount: float - currency: str - - -@workflows.activity( - start_to_close_timeout=timedelta(minutes=30), - retry_policy_max_attempts=5, -) -async def extract_invoice(doc_url: str) -> InvoiceData: - out = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code="finance.extract_invoice", - inputs={"doc_url": doc_url}, - ) - ) - main_stuff = out.output_dict["root"][out.main_stuff_name] - return InvoiceData.model_validate(main_stuff["content"]) -``` - -The `run_pipe_via_bridge` helper is the same code the Tier 1 activity calls — just without the decoration. This is the recommended tier for production: you keep typed activity inputs/outputs, custom retries per pipe, and you can register multiple pipe-specific activities (`extract_invoice`, `summarize_contract`, ...) on the same Mistral worker. - ---- - -## Tier 3 — full control (`library_crate_dump`) - -Tier 3 is for cases where the Pipelex bundle is not pre-loaded into the worker's global registry — for example, the calling Mistral workflow received the bundle as part of an API request and needs to run a pipe defined in it without polluting the shared library. - -```python -from mistralai import workflows - -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - run_pipe_via_bridge, -) - - -@workflows.activity() -async def run_user_supplied_pipe( - pipe_code: str, - inputs: dict, - library_crate_dump: dict, -) -> dict: - out = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=pipe_code, - inputs=inputs, - library_crate_dump=library_crate_dump, - ) - ) - return out.output_dict -``` - -The bridge opens a per-call scoped library, loads the crate, runs the pipe, and tears the scope down on the way out. The global registry is untouched — concurrent activities with different `library_crate_dump`s do not see each other's classes. - -To produce the dump on the submitter side: - -```python -from pipelex.hub import get_library_manager - -crate = get_library_manager().get_crate(library_id=my_lib_id) -crate_dump = crate.model_dump(mode="json") -``` - ---- - -## Picking an execution mode - -| You want… | Use | -| -------------------------------------------------------------------- | ---------------------------- | -| Run a pipe in-process inside the Mistral activity | `DIRECT` | -| Hand off pipe execution to your existing Pipelex Temporal cluster | `TEMPORAL_BLOCKING` | -| Don't block the activity for a long-running pipe; deliver out-of-band | `TEMPORAL_FIRE_AND_FORGET` | - -`TEMPORAL_FIRE_AND_FORGET` requires `delivery_assignment_dump` so the completion can reach somebody — webhook, storage target, or both. - -```python -from pipelex.pipe_run.delivery_assignment import ( - DeliveryAssignment, - StorageTarget, - WebhookTarget, -) - -delivery = DeliveryAssignment( - storage=StorageTarget(key_prefix="invoices/2026/"), - webhooks=[WebhookTarget(url="https://my.app/pipelex-callback")], -) - -result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code="finance.extract_invoice", - inputs={"doc_url": doc_url}, - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - delivery_assignment_dump=delivery.model_dump(mode="json"), - ) -) -# result.is_completed is False -# result.workflow_id is the Pipelex Temporal workflow id -# Completion arrives at the webhook + storage location later. -``` diff --git a/mkdocs.yml b/mkdocs.yml index a5e51a222..1ce9f4528 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -307,8 +307,6 @@ plugins: - under-the-hood/pipe-routing-and-execution.md: "Pipe Routing & Execution" - under-the-hood/temporal-integration.md: "Temporal Integration" - under-the-hood/distributed-content-generation.md: "Distributed Content Generation" - - under-the-hood/mistralai-workflows-plugin.md: "Mistral Workflows Plugin" - - under-the-hood/mistralai-workflows-recipes.md: "Mistral Workflows Recipes" Project: - contributing.md: "Contributing" - contribute/configuration-defaults-and-overrides.md: "Configuration Internals" @@ -497,8 +495,6 @@ nav: - Pipe Routing & Execution: under-the-hood/pipe-routing-and-execution.md - Temporal Integration: under-the-hood/temporal-integration.md - Distributed Content Generation: under-the-hood/distributed-content-generation.md - - Mistral Workflows Plugin: under-the-hood/mistralai-workflows-plugin.md - - Mistral Workflows Recipes: under-the-hood/mistralai-workflows-recipes.md - Project: - Contributing: contributing.md - Configuration Internals: contribute/configuration-defaults-and-overrides.md diff --git a/pipelex/plugins/mistralai_workflows/__init__.py b/pipelex/plugins/mistralai_workflows/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/pipelex/plugins/mistralai_workflows/activities.py b/pipelex/plugins/mistralai_workflows/activities.py deleted file mode 100644 index 76da0755f..000000000 --- a/pipelex/plugins/mistralai_workflows/activities.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Tier 1 — pre-decorated Mistral Workflows activity that runs a Pipelex pipe. - -Importing this module triggers the optional-dep guard: if -``mistralai-workflows`` is not installed, the import fails fast with a -``MistralWorkflowsNotInstalledError`` carrying install instructions. - -Two activity variants are exposed: - -- ``pipelex_run_pipe`` — inline boundary types. Use when payloads stay below - Temporal's per-event size limit (~2 MiB). -- ``pipelex_run_pipe_offloaded`` — boundary types wrapped in - ``OffloadableField`` so Mistral's ``ActivityInOutOffloadingInterceptor`` - can stream the payload through blob storage when it exceeds the configured - threshold. Requires the user to register the interceptor on their worker - (see Mistral's ``workflow_activity_offloading`` example). -""" - -from datetime import timedelta - -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError - -try: - from mistralai.workflows import activity - from mistralai.workflows.core.encoding.fields_offloader import OffloadableField, OffloadableModel -except ImportError as exc: - msg = ( - "The 'mistralai-workflows' optional dependency is required to use " - "pipelex.plugins.mistralai_workflows.activities. " - "Install with: pip install 'pipelex[mistralai-workflows]'" - ) - raise MistralWorkflowsNotInstalledError(msg) from exc - - -class PipelexPipeRunInputOffloaded(OffloadableModel): - """Offload-capable variant of ``PipelexPipeRunInput``. - - Wraps the inline ``PipelexPipeRunInput`` in an ``OffloadableField`` so the - ``ActivityInOutOffloadingInterceptor`` can stream the payload to blob - storage when its serialized size exceeds the configured threshold. - """ - - payload: OffloadableField[PipelexPipeRunInput] - - -class PipelexPipeRunOutputOffloaded(OffloadableModel): - """Offload-capable variant of ``PipelexPipeRunOutput``. - - Mirrors ``PipelexPipeRunInputOffloaded`` for the return path. - """ - - payload: OffloadableField[PipelexPipeRunOutput] - - -@activity( - start_to_close_timeout=timedelta(minutes=10), - retry_policy_max_attempts=3, -) -async def pipelex_run_pipe(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: - """Run a Pipelex pipe from inside a Mistral Workflows activity. - - Thin wrapper around ``run_pipe_via_bridge`` so users get a ready-to-register - activity without having to write their own ``@activity`` decoration. For - custom timeouts, retry policies, rate limits, or sticky-to-worker config, - call ``run_pipe_via_bridge`` directly from your own ``@activity`` (Tier 2). - """ - return await run_pipe_via_bridge(input_payload) - - -@activity( - start_to_close_timeout=timedelta(minutes=10), - retry_policy_max_attempts=3, -) -async def pipelex_run_pipe_offloaded( - input_payload: PipelexPipeRunInputOffloaded, -) -> PipelexPipeRunOutputOffloaded: - """Run a Pipelex pipe with offload-capable boundary types. - - Same semantics as ``pipelex_run_pipe`` but the input/output are wrapped - in ``OffloadableField``. To actually offload payloads to blob storage, - the worker must be configured with ``ActivityInOutOffloadingInterceptor`` - pointing at S3/GCS/Azure (see Mistral's - ``workflow_activity_offloading`` example). Without that interceptor, the - payload still rides inline through Temporal and offloading is a no-op. - """ - pipe_input = input_payload.payload.get_value() - pipe_output = await run_pipe_via_bridge(pipe_input) - return PipelexPipeRunOutputOffloaded( - payload=OffloadableField(value=pipe_output), - ) diff --git a/pipelex/plugins/mistralai_workflows/bootstrap.py b/pipelex/plugins/mistralai_workflows/bootstrap.py deleted file mode 100644 index b607aeac3..000000000 --- a/pipelex/plugins/mistralai_workflows/bootstrap.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Idempotent Pipelex boot helpers for use inside Mistral Workflows activities. - -Pipelex's own ``Pipelex.make()`` raises if a singleton already exists. The -activity boundary is a hot path that can be reached from many concurrent -activities, so we wrap the boot in an idempotent guard so callers don't have -to think about it. -""" - -from typing import Any, Callable - -from pipelex.pipelex import Pipelex - - -def ensure_pipelex_booted( - config_overrides: dict[str, Any] | None = None, -) -> None: - """Boot Pipelex on first call; no-op if already initialized. - - Idempotent. Safe to call from inside an activity; safe to call from a - worker entry-point before activities start. If a Pipelex singleton was - already created externally (e.g. via the user's worker bootstrap), this - function adopts that singleton without re-initializing. - """ - if Pipelex.get_optional_instance() is None: - Pipelex.make(config_overrides=config_overrides) - - -def get_pipelex_dependency() -> Callable[[], Pipelex]: - """Return a callable suitable for ``mistralai.workflows.Depends(...)``. - - Booting on first resolve so the dependency is cheap to declare per-activity - without forcing eager init at worker start. - """ - - def _resolver() -> Pipelex: - ensure_pipelex_booted() - return Pipelex.get_instance() - - return _resolver diff --git a/pipelex/plugins/mistralai_workflows/bridge.py b/pipelex/plugins/mistralai_workflows/bridge.py deleted file mode 100644 index 950aaf3c7..000000000 --- a/pipelex/plugins/mistralai_workflows/bridge.py +++ /dev/null @@ -1,337 +0,0 @@ -"""Framework-agnostic core of the mistralai_workflows plugin. - -This module contains the boundary types (``PipelexPipeRunInput`` / -``PipelexPipeRunOutput``) and the dispatch entry-point -(``run_pipe_via_bridge``) used by the Mistral Workflows activity wrapper. It -deliberately does NOT import ``mistralai.workflows`` at module top-level so -that callers can use the bridge directly (Tier 3 usage) and so that unit tests -can exercise it on a venv without the optional dep installed. - -The Temporal extra is lazy-imported only inside the temporal-mode branches. -""" - -from __future__ import annotations - -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, Any, AsyncGenerator -from uuid import uuid4 - -import shortuuid -from pydantic import BaseModel, ConfigDict, Field - -from pipelex.core.memory.working_memory import MAIN_STUFF_NAME -from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory -from pipelex.hub import ( - get_library_manager, - get_required_pipe, - set_current_library, - teardown_current_library, -) -from pipelex.libraries.library_crate import LibraryCrate -from pipelex.pipe_run.delivery_assignment import DeliveryAssignment -from pipelex.pipe_run.exceptions import PipeJobError, PipeRouterError, PipeRunError -from pipelex.pipe_run.pipe_job_factory import PipeJobFactory -from pipelex.pipe_run.pipe_router import PipeRouter -from pipelex.pipe_run.pipe_run import PipeRun -from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory -from pipelex.pipeline.exceptions import PipeExecutionError, PipelineExecutionError -from pipelex.pipeline.job_metadata import JobMetadata -from pipelex.plugins.mistralai_workflows.bootstrap import ensure_pipelex_booted -from pipelex.plugins.mistralai_workflows.exceptions import ( - MissingPipelexTemporalExtraError, - PipelexBridgeRuntimeError, -) -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.system.telemetry.otel_constants import OTelConstants - -if TYPE_CHECKING: - from pipelex.core.memory.working_memory import WorkingMemory - from pipelex.core.pipes.pipe_output import PipeOutput - from pipelex.graph.graph_context import GraphContext - from pipelex.pipe_run.pipe_job import PipeJob - - -class PipelexPipeRunInput(BaseModel): - """JSON-safe input crossing the Mistral/Temporal boundary.""" - - model_config = ConfigDict(extra="forbid") - - pipe_code: str - inputs: dict[str, Any] = Field(default_factory=dict) - output_name: str | None = None - pipeline_run_id: str | None = None - user_id: str | None = None - library_crate_dump: dict[str, Any] | None = None - execution_mode: PipelexExecutionMode = PipelexExecutionMode.DIRECT - delivery_assignment_dump: dict[str, Any] | None = None - - -class PipelexPipeRunOutput(BaseModel): - """JSON-safe output crossing the Mistral/Temporal boundary.""" - - model_config = ConfigDict(extra="forbid") - - output_dict: dict[str, Any] - main_stuff_name: str | None = None - pipeline_run_id: str - workflow_id: str | None = None - is_completed: bool - graph_spec_dump: dict[str, Any] | None = None - - -async def run_pipe_via_bridge( - input_payload: PipelexPipeRunInput, - graph_context: GraphContext | None = None, -) -> PipelexPipeRunOutput: - """Run a Pipelex pipe from inside a Mistral Workflows activity. - - Booting Pipelex on first call (no-op if already initialized); validating - the input; opening a per-call scoped library if a ``library_crate_dump`` - is provided; then dispatching to the requested execution mode. - - The optional ``graph_context`` is plumbed into ``JobMetadata`` so callers - (e.g. the streaming activity) that already opened a - ``GraphTracerManager`` tracer for this pipeline run get per-step trace - events flowing through the configured event log. ``graph_context`` is - only honored for ``DIRECT`` execution mode — TEMPORAL modes already - have their own event-log infrastructure via ``pipeline_run_setup`` and - a passed-in context would be ignored anyway. - """ - ensure_pipelex_booted() - _validate_input(input_payload) - - library_crate = _decode_library_crate(input_payload.library_crate_dump) - delivery_assignment = _decode_delivery_assignment(input_payload.delivery_assignment_dump) - - async with _scoped_library_for_crate(library_crate): - pipe_job = build_pipe_job_from_input( - input_payload=input_payload, - library_crate=library_crate, - graph_context=graph_context, - ) - - match input_payload.execution_mode: - case PipelexExecutionMode.DIRECT: - return await _run_direct(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - case PipelexExecutionMode.TEMPORAL_BLOCKING: - _require_pipelex_temporal_extra() - return await _run_temporal_blocking(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: - _require_pipelex_temporal_extra() - return await _run_temporal_fire_and_forget(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - - -def build_pipe_job_from_input( - input_payload: PipelexPipeRunInput, - library_crate: LibraryCrate | None, - graph_context: GraphContext | None = None, -) -> PipeJob: - """Hydrate a PipeJob from JSON-safe input. - - Looks up the pipe in the active library; the caller is responsible for - making sure the active library contains the pipe (by passing a - ``library_crate_dump`` or pre-loading the library at boot). - - The optional ``graph_context`` is plumbed into ``JobMetadata`` so a - caller (e.g. the streaming activity) that has already opened a - ``GraphTracerManager`` tracer for this pipeline run can have per-step - ``PipeStartEvent`` / ``PipeEndSuccessEvent`` events flow through the - pipe execution. When ``None``, no tracing happens (current default). - """ - pipe = get_required_pipe(pipe_code=input_payload.pipe_code) - - pipeline_run_id = input_payload.pipeline_run_id or shortuuid.uuid() - - working_memory: WorkingMemory - if input_payload.inputs: - working_memory = WorkingMemoryFactory.make_from_pipeline_inputs( - pipeline_inputs=input_payload.inputs, - search_domain_codes=[pipe.domain_code], - ) - else: - working_memory = WorkingMemoryFactory.make_empty() - - job_metadata = JobMetadata( - user_id=input_payload.user_id or OTelConstants.DEFAULT_USER_ID, - pipeline_run_id=pipeline_run_id, - graph_context=graph_context, - ) - pipe_run_params = PipeRunParamsFactory.make_run_params() - - return PipeJobFactory.make_pipe_job( - pipe=pipe, - pipe_run_params=pipe_run_params, - job_metadata=job_metadata, - working_memory=working_memory, - output_name=input_payload.output_name, - library_crate=library_crate, - ) - - -def serialize_pipe_output(pipe_output: PipeOutput) -> dict[str, Any]: - """Dehydrate a PipeOutput's working memory to a JSON-safe dict. - - Always uses ``WorkingMemory.dump_for_temporal()`` — the same format Pipelex - uses internally for Temporal transit. The shape is stable regardless of - whether a ``library_crate`` was attached: - ``{"root": {stuff_name: {"content": {...}, ...}}, "aliases": {...}}``. - - Type metadata embedded by ``dump_for_temporal`` lets callers reconstruct a - typed ``WorkingMemory`` when they have the matching class registry in - scope (e.g. via ``hydrate_working_memory``). - """ - return pipe_output.working_memory.dump_for_temporal() - - -def _validate_input(input_payload: PipelexPipeRunInput) -> None: - if input_payload.execution_mode is PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET and input_payload.delivery_assignment_dump is None: - msg = ( - "PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET requires a delivery_assignment_dump; " - "otherwise the pipe completion would be silently dropped." - ) - raise PipelexBridgeRuntimeError(msg) - - -def _decode_library_crate(library_crate_dump: dict[str, Any] | None) -> LibraryCrate | None: - if library_crate_dump is None: - return None - return LibraryCrate.model_validate(library_crate_dump) - - -def _decode_delivery_assignment(delivery_assignment_dump: dict[str, Any] | None) -> DeliveryAssignment | None: - if delivery_assignment_dump is None: - return None - return DeliveryAssignment.model_validate(delivery_assignment_dump) - - -@asynccontextmanager -async def _scoped_library_for_crate(library_crate: LibraryCrate | None) -> AsyncGenerator[str | None, None]: # noqa: RUF029 - """Open a per-call scoped library for the duration of a pipe run. - - When ``library_crate`` is None, this is a no-op: callers fall back to the - library that was loaded into the active class registry at boot. When - provided, opens a fresh library, loads the crate into it, sets it as the - current library for the duration of the pipe execution, and tears it down - on the way out. - """ - if library_crate is None: - yield None - return - - library_manager = get_library_manager() - library_id = f"mistralai_workflows_{uuid4().hex[:8]}" - library_manager.open_library(library_id=library_id) - set_current_library(library_id=library_id) - try: - library_manager.load_from_crate(library_id=library_id, crate=library_crate) - yield library_id - finally: - library_manager.teardown(library_id=library_id) - teardown_current_library() - - -async def _run_direct( - pipe_job: PipeJob, - delivery_assignment: DeliveryAssignment | None, -) -> PipelexPipeRunOutput: - pipe_run = PipeRun(pipe_router=PipeRouter()) - try: - pipe_output = await pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: - msg = f"Pipe execution failed in DIRECT mode for pipe '{pipe_job.pipe.code}': {exc}" - raise PipelexBridgeRuntimeError(msg) from exc - - return _serialize_completed_output( - pipe_output=pipe_output, - pipe_job=pipe_job, - workflow_id=None, - ) - - -async def _run_temporal_blocking( - pipe_job: PipeJob, - delivery_assignment: DeliveryAssignment | None, -) -> PipelexPipeRunOutput: - from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 - - temporal_pipe_run = make_temporal_pipe_run() - try: - pipe_output = await temporal_pipe_run.run(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: - msg = f"Pipe execution failed in TEMPORAL_BLOCKING mode for pipe '{pipe_job.pipe.code}': {exc}" - raise PipelexBridgeRuntimeError(msg) from exc - - return _serialize_completed_output( - pipe_output=pipe_output, - pipe_job=pipe_job, - workflow_id=pipe_output.pipeline_run_id, - ) - - -async def _run_temporal_fire_and_forget( - pipe_job: PipeJob, - delivery_assignment: DeliveryAssignment | None, -) -> PipelexPipeRunOutput: - from pipelex.temporal.tprl_pipe.temporal_pipe_run import make_temporal_pipe_run # noqa: PLC0415 - - temporal_pipe_run = make_temporal_pipe_run() - try: - workflow_id, _handle = await temporal_pipe_run.start(pipe_job=pipe_job, delivery_assignment=delivery_assignment) - except (PipeRunError, PipeJobError, PipeRouterError, PipeExecutionError, PipelineExecutionError) as exc: - msg = f"Pipe dispatch failed in TEMPORAL_FIRE_AND_FORGET mode for pipe '{pipe_job.pipe.code}': {exc}" - raise PipelexBridgeRuntimeError(msg) from exc - - return PipelexPipeRunOutput( - output_dict={}, - main_stuff_name=None, - pipeline_run_id=pipe_job.job_metadata.pipeline_run_id, - workflow_id=workflow_id, - is_completed=False, - graph_spec_dump=None, - ) - - -def _serialize_completed_output( - pipe_output: PipeOutput, - pipe_job: PipeJob, # noqa: ARG001 — kept for symmetry with future per-crate serialization tweaks - workflow_id: str | None, -) -> PipelexPipeRunOutput: - output_dict = serialize_pipe_output(pipe_output=pipe_output) - - main_stuff_name = _resolve_main_stuff_root_key(pipe_output=pipe_output) - - graph_spec_dump = pipe_output.graph_spec.model_dump(mode="json") if pipe_output.graph_spec is not None else None - - return PipelexPipeRunOutput( - output_dict=output_dict, - main_stuff_name=main_stuff_name, - pipeline_run_id=pipe_output.pipeline_run_id, - workflow_id=workflow_id, - is_completed=True, - graph_spec_dump=graph_spec_dump, - ) - - -def _resolve_main_stuff_root_key(pipe_output: PipeOutput) -> str | None: - """Return the actual ``root`` dict key under which the main stuff lives. - - The main stuff can either sit directly at ``root[MAIN_STUFF_NAME]`` or be - referenced via ``aliases[MAIN_STUFF_NAME]`` pointing at its real name. - Callers indexing the output_dict need the actual root key, not the - stuff's display ``stuff_name``. - """ - working_memory = pipe_output.working_memory - if MAIN_STUFF_NAME in working_memory.root: - return MAIN_STUFF_NAME - aliased_target = working_memory.aliases.get(MAIN_STUFF_NAME) - if aliased_target is not None and aliased_target in working_memory.root: - return aliased_target - return None - - -def _require_pipelex_temporal_extra() -> None: - try: - import temporalio # noqa: F401, PLC0415 - except ImportError as exc: - msg = "TEMPORAL_* execution modes require the pipelex[temporal] extra. Install with: pip install 'pipelex[temporal,mistralai-workflows]'" - raise MissingPipelexTemporalExtraError(msg) from exc diff --git a/pipelex/plugins/mistralai_workflows/exceptions.py b/pipelex/plugins/mistralai_workflows/exceptions.py deleted file mode 100644 index 7d04d7908..000000000 --- a/pipelex/plugins/mistralai_workflows/exceptions.py +++ /dev/null @@ -1,17 +0,0 @@ -from pipelex.base_exceptions import PipelexError - - -class MistralWorkflowsPluginError(PipelexError): - """Base for errors raised by the mistralai-workflows plugin.""" - - -class MistralWorkflowsNotInstalledError(MistralWorkflowsPluginError, ImportError): - """Raised when the optional `mistralai-workflows` dependency is missing.""" - - -class MissingPipelexTemporalExtraError(MistralWorkflowsPluginError): - """Raised when a TEMPORAL_* execution mode is requested without the pipelex[temporal] extra.""" - - -class PipelexBridgeRuntimeError(MistralWorkflowsPluginError): - """Raised when a pipe execution dispatched through the bridge fails.""" diff --git a/pipelex/plugins/mistralai_workflows/execution_mode.py b/pipelex/plugins/mistralai_workflows/execution_mode.py deleted file mode 100644 index 24f3f6196..000000000 --- a/pipelex/plugins/mistralai_workflows/execution_mode.py +++ /dev/null @@ -1,37 +0,0 @@ -from pipelex.types import StrEnum - - -class PipelexExecutionMode(StrEnum): - """How a Pipelex pipe runs inside a Mistral Workflows activity. - - DIRECT: in-process; no Temporal involved on Pipelex's side; activity blocks - until the pipe completes. Fastest feedback, simplest ops. - TEMPORAL_BLOCKING: dispatch the pipe as a Pipelex Temporal workflow; the - activity awaits completion. Pipe runs durably on Pipelex's worker - fleet. Requires the pipelex[temporal] extra. - TEMPORAL_FIRE_AND_FORGET: dispatch the pipe as a Pipelex Temporal workflow - and return immediately with the workflow_id. Activity does NOT wait; - completion is signalled out-of-band via DeliveryAssignment (webhook / - storage). Same dep requirements as TEMPORAL_BLOCKING. - ``delivery_assignment_dump`` is required. - """ - - DIRECT = "direct" - TEMPORAL_BLOCKING = "temporal_blocking" - TEMPORAL_FIRE_AND_FORGET = "temporal_fire_and_forget" - - @property - def requires_pipelex_temporal(self) -> bool: - match self: - case PipelexExecutionMode.DIRECT: - return False - case PipelexExecutionMode.TEMPORAL_BLOCKING | PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: - return True - - @property - def is_fire_and_forget(self) -> bool: - match self: - case PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET: - return True - case PipelexExecutionMode.DIRECT | PipelexExecutionMode.TEMPORAL_BLOCKING: - return False diff --git a/pipelex/plugins/mistralai_workflows/streaming.py b/pipelex/plugins/mistralai_workflows/streaming.py deleted file mode 100644 index 299150c0a..000000000 --- a/pipelex/plugins/mistralai_workflows/streaming.py +++ /dev/null @@ -1,226 +0,0 @@ -"""Phase 2.1 — streaming variant of the Pipelex bridge activity. - -Wraps a Pipelex pipe run in a single Mistral Workflows ``Task`` so subscribers -can observe progress through ``CustomTaskStarted`` / ``CustomTaskInProgress`` -/ ``CustomTaskCompleted`` / ``CustomTaskFailed`` events. - -Phase 2.0 emitted exactly two state transitions per call (``started`` / -``completed``). Phase 2.1 adds **per-step granularity** for ``DIRECT`` mode: -the activity opens a per-call ``GraphTracerManager`` tracer with a -queue-backed event log injected, and an asyncio forwarder drains the queue -into ``Task.update_state`` so each Pipelex pipe boundary produces a -``CustomTaskInProgress`` event. ``TEMPORAL_BLOCKING`` and -``TEMPORAL_FIRE_AND_FORGET`` keep Phase 2.0 behavior — per-step streaming -across the Temporal worker boundary is a future phase. - -Importing this module triggers the optional-dep guard: if -``mistralai-workflows`` is not installed, the import fails fast with a -``MistralWorkflowsNotInstalledError`` carrying install instructions. The -sibling ``activities`` module follows the same pattern. -""" - -from __future__ import annotations - -import asyncio -from datetime import timedelta -from typing import Any - -import shortuuid -from pydantic import BaseModel, ConfigDict - -from pipelex.graph.graph_tracer_manager import GraphTracerManager -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.exceptions import MistralWorkflowsNotInstalledError -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.plugins.mistralai_workflows.streaming_event_forwarder import ( - SHUTDOWN_SENTINEL, - QueueEventLog, - build_streaming_data_inclusion, - forward_events_to_task, - get_drain_timeout_seconds, - get_queue_max_size, -) - -try: - from mistralai.workflows import activity - from mistralai.workflows.core.task import Task -except ImportError as exc: - msg = ( - "The 'mistralai-workflows' optional dependency is required to use " - "pipelex.plugins.mistralai_workflows.streaming. " - "Install with: pip install 'pipelex[mistralai-workflows]'" - ) - raise MistralWorkflowsNotInstalledError(msg) from exc - - -PIPELEX_PIPE_RUN_TASK_TYPE = "pipelex.pipe_run" - - -class PipelexPipeRunStreamingState(BaseModel): - """Observable state surfaced through Mistral's Task API for a Pipelex pipe run. - - Phase 2.0 fields (always present): - - - ``phase``: one of ``"started"`` / ``"in_progress"`` / ``"completed"``. - ``"failed"`` is not written explicitly — ``Task.__aexit__`` emits - ``CustomTaskFailed`` on exception and the original exception - propagates. - - ``pipe_code`` / ``execution_mode`` / ``pipeline_run_id`` / - ``main_stuff_name``: identifiers, set on ``started`` and refined on - ``completed``. - - Phase 2.1 fields (only populated for DIRECT mode runs that go through - ``pipelex_run_pipe_streaming``; remain at defaults for TEMPORAL modes): - - - ``current_step_pipe_code`` / ``current_step_node_id``: identify the - pipe boundary that just fired. - - ``last_event_kind``: ``"pipe_start"`` / ``"pipe_end_success"`` / - ``"pipe_end_error"`` — lets subscribers route on event type. - - ``started_steps`` / ``completed_steps``: cumulative counters, - monotonic, 1-indexed. - - ``last_output_stuff_name``: the IOSpec name of the most recent - successful step's output, or ``None`` if the step had no output spec. - """ - - model_config = ConfigDict(extra="forbid") - - phase: str - pipe_code: str - execution_mode: str - pipeline_run_id: str | None = None - main_stuff_name: str | None = None - - current_step_pipe_code: str | None = None - current_step_node_id: str | None = None - last_event_kind: str | None = None - started_steps: int = 0 - completed_steps: int = 0 - last_output_stuff_name: str | None = None - - -@activity( - start_to_close_timeout=timedelta(minutes=10), - retry_policy_max_attempts=3, -) -async def pipelex_run_pipe_streaming(input_payload: PipelexPipeRunInput) -> PipelexPipeRunOutput: - """Streaming variant of ``pipelex_run_pipe``. - - Same semantics as :func:`pipelex_run_pipe` but wraps the bridge call in a - single Mistral ``Task`` whose lifecycle (``started``, ``in_progress``, - ``completed`` / ``failed``) is published to whichever events client the - worker is configured with. For the silent path (no observability needed) - use ``pipelex_run_pipe`` instead — the streaming variant adds a small - constant overhead per activity for the lifecycle events. - - For ``DIRECT`` execution mode, opens a per-call ``GraphTracerManager`` - tracer with an in-process queue-backed event log; spawns a forwarder - coroutine that translates each ``PipeStartEvent`` / - ``PipeEndSuccessEvent`` / ``PipeEndErrorEvent`` into a - ``Task.update_state`` call so subscribers see one - ``CustomTaskInProgress`` per pipe boundary. ``TEMPORAL_*`` modes keep - the Phase 2.0 single-pair behavior. - """ - pipeline_run_id = input_payload.pipeline_run_id or shortuuid.uuid() - if input_payload.pipeline_run_id is None: - input_payload = input_payload.model_copy(update={"pipeline_run_id": pipeline_run_id}) - - initial_state = PipelexPipeRunStreamingState( - phase="started", - pipe_code=input_payload.pipe_code, - execution_mode=input_payload.execution_mode, - pipeline_run_id=pipeline_run_id, - ) - - if input_payload.execution_mode is PipelexExecutionMode.DIRECT: - return await _run_streaming_with_per_step_events( - input_payload=input_payload, - pipeline_run_id=pipeline_run_id, - initial_state=initial_state, - ) - - return await _run_streaming_without_per_step_events( - input_payload=input_payload, - initial_state=initial_state, - ) - - -async def _run_streaming_with_per_step_events( - input_payload: PipelexPipeRunInput, - pipeline_run_id: str, - initial_state: PipelexPipeRunStreamingState, -) -> PipelexPipeRunOutput: - """DIRECT-mode streaming path — opens a tracer + forwarder for per-step events.""" - event_queue: asyncio.Queue[Any] = asyncio.Queue(maxsize=get_queue_max_size()) - queue_event_log = QueueEventLog(loop=asyncio.get_running_loop(), queue=event_queue) - tracer_manager = GraphTracerManager.get_or_create_instance() - graph_context = tracer_manager.open_tracer( - graph_id=pipeline_run_id, - data_inclusion=build_streaming_data_inclusion(), - pipeline_ref_domain=None, - pipeline_ref_main_pipe=None, - event_log=queue_event_log, - workflow_id="direct", - pipeline_run_id=pipeline_run_id, - ) - - try: - async with Task[PipelexPipeRunStreamingState]( - type=PIPELEX_PIPE_RUN_TASK_TYPE, - state=initial_state, - ) as streaming_task: - forwarder_task = asyncio.create_task( - forward_events_to_task( - event_queue=event_queue, - update_state=streaming_task.update_state, - ), - name=f"pipelex-streaming-forwarder-{pipeline_run_id}", - ) - try: - output = await run_pipe_via_bridge(input_payload, graph_context=graph_context) - finally: - # Drain ALL pending per-step events BEFORE writing the final - # "completed" state. If we wrote phase="completed" first, the - # forwarder's still-pending pipe_end_success patches would race - # the snapshot and the captured CustomTaskCompleted event would - # read phase="in_progress". On the failure path, the drain - # also lets pending in-progress events publish before the - # surrounding ``async with Task`` emits CustomTaskFailed. - event_queue.put_nowait(SHUTDOWN_SENTINEL) - try: - await asyncio.wait_for(forwarder_task, timeout=get_drain_timeout_seconds()) - except TimeoutError: - forwarder_task.cancel() - await streaming_task.update_state( - { - "phase": "completed", - "pipeline_run_id": output.pipeline_run_id, - "main_stuff_name": output.main_stuff_name, - } - ) - return output - finally: - tracer_manager.close_tracer(pipeline_run_id) - - -async def _run_streaming_without_per_step_events( - input_payload: PipelexPipeRunInput, - initial_state: PipelexPipeRunStreamingState, -) -> PipelexPipeRunOutput: - """TEMPORAL-mode streaming path — Phase 2.0 single-pair semantics, no tracer.""" - async with Task[PipelexPipeRunStreamingState]( - type=PIPELEX_PIPE_RUN_TASK_TYPE, - state=initial_state, - ) as streaming_task: - output = await run_pipe_via_bridge(input_payload) - await streaming_task.update_state( - { - "phase": "completed", - "pipeline_run_id": output.pipeline_run_id, - "main_stuff_name": output.main_stuff_name, - } - ) - return output diff --git a/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py b/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py deleted file mode 100644 index c3420c4b1..000000000 --- a/pipelex/plugins/mistralai_workflows/streaming_event_forwarder.py +++ /dev/null @@ -1,284 +0,0 @@ -"""Per-step event forwarding for the streaming activity (Phase 2.1). - -Bridges Pipelex's trace event channel into Mistral's ``Task.update_state``. -``streaming.py`` opens a per-call ``GraphTracerManager`` tracer with a -``QueueEventLog`` injected as the event log, then spawns -``forward_events_to_task`` to drain the queue and translate trace events -into ``Task.update_state(...)`` calls. - -This module is framework-agnostic: it does NOT import ``mistralai.workflows``. -The forwarder takes the bound ``update_state`` coroutine as a callable so the -``Task`` type can stay isolated to ``streaming.py``. -""" - -from __future__ import annotations - -import asyncio -import threading -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Final, Literal - -from typing_extensions import override - -from pipelex import log -from pipelex.graph.graph_config import DataInclusionConfig -from pipelex.tracing.event_log_protocol import EventLogProtocol -from pipelex.tracing.trace_events import ( - BatchAggregateEvent, - BatchItemEvent, - ControllerOutputEvent, - EdgeEvent, - ExecutionDataEvent, - ParallelCombineEvent, - PipeEndErrorEvent, - PipeEndSuccessEvent, - PipeStartEvent, - TraceEvent, - UsageReportEvent, -) - -if TYPE_CHECKING: - from collections.abc import Awaitable, Callable - -_QUEUE_MAX_SIZE: Final[int] = 256 -_FORWARDER_DRAIN_TIMEOUT_SECONDS: Final[float] = 5.0 - - -class _ShutdownSentinel: - """Module-private sentinel type pushed onto the queue to stop the forwarder.""" - - -SHUTDOWN_SENTINEL: Final[_ShutdownSentinel] = _ShutdownSentinel() - - -_StatePatchKind = Literal["pipe_start", "pipe_end_success", "pipe_end_error"] - - -@dataclass(frozen=True) -class _StatePatch: - """A single Mistral ``Task.update_state`` payload derived from a trace event.""" - - kind: _StatePatchKind - payload: dict[str, Any] - - -class QueueEventLog(EventLogProtocol): - """In-process ``EventLogProtocol`` that pushes events onto an asyncio queue. - - Used by the streaming activity to subscribe to per-step trace events - without persisting them. ``emit`` is called by ``GraphTracer`` from the - pipe-execution context (which may be a worker thread for inference jobs), - so the queue insert is always routed via ``loop.call_soon_threadsafe``. - - Best-effort delivery: when the bounded queue is full, events are dropped - with a one-shot warning. Streaming is observability, not durability. - """ - - def __init__(self, loop: asyncio.AbstractEventLoop, queue: asyncio.Queue[Any]) -> None: - self._loop = loop - self._queue = queue - self._sequence: int = 0 - self._sequence_lock = threading.Lock() - self._writer_id = "mistralai-workflows-streaming" - self._closed = False - self._overflow_warned = False - - @property - @override - def writer_id(self) -> str: - return self._writer_id - - @override - def next_sequence(self) -> int: - with self._sequence_lock: - seq = self._sequence - self._sequence += 1 - return seq - - @override - def emit(self, event: TraceEvent) -> None: - """Push the event onto the queue, routing across threads if needed. - - ``call_soon_threadsafe`` is correct from a worker thread but defers - execution to the next loop iteration; in our hot path the trace - events fire on the same loop as the activity, so we ``put_nowait`` - directly to keep the forwarder fed without an extra trampoline. - """ - if self._closed: - return - try: - running_loop = asyncio.get_running_loop() - except RuntimeError: - running_loop = None - if running_loop is self._loop: - self._enqueue(event) - else: - self._loop.call_soon_threadsafe(self._enqueue, event) - - def _enqueue(self, event: TraceEvent) -> None: - try: - self._queue.put_nowait(event) - except asyncio.QueueFull: - if not self._overflow_warned: - self._overflow_warned = True - log.warning( - f"mistralai_workflows streaming forwarder queue full (maxsize={_QUEUE_MAX_SIZE}); dropping further events for this run.", - ) - - @override - def read_events(self, pipeline_run_id: str) -> list[TraceEvent]: - return [] - - @override - def close(self) -> None: - self._closed = True - - @override - def cleanup(self, pipeline_run_id: str) -> None: - return None - - -def build_streaming_data_inclusion() -> DataInclusionConfig: - """All-flags-off ``DataInclusionConfig`` for the streaming tracer. - - Phase 2.1 only needs pipe metadata (codes, node ids, output spec name); - capturing rendered content / stack traces / registry dumps would just - bloat ``Task.update_state`` payloads with no consumer benefit. - """ - return DataInclusionConfig( - stuff_json_content=False, - stuff_text_content=False, - stuff_html_content=False, - error_stack_traces=False, - pipe_and_concept_registry=False, - ) - - -def _state_patch_for_pipe_start(event: PipeStartEvent, started_steps: int) -> _StatePatch: - return _StatePatch( - kind="pipe_start", - payload={ - "phase": "in_progress", - "current_step_pipe_code": event.pipe_code, - "current_step_node_id": event.node_id, - "last_event_kind": "pipe_start", - "started_steps": started_steps, - }, - ) - - -def _state_patch_for_pipe_end_success(event: PipeEndSuccessEvent, completed_steps: int) -> _StatePatch: - output_stuff_name: str | None = None - if event.output_spec is not None: - output_stuff_name = event.output_spec.name - return _StatePatch( - kind="pipe_end_success", - payload={ - "phase": "in_progress", - "last_event_kind": "pipe_end_success", - "completed_steps": completed_steps, - "last_output_stuff_name": output_stuff_name, - }, - ) - - -def _state_patch_for_pipe_end_error(event: PipeEndErrorEvent) -> _StatePatch: - return _StatePatch( - kind="pipe_end_error", - payload={ - "phase": "in_progress", - "last_event_kind": "pipe_end_error", - "current_step_node_id": event.node_id, - }, - ) - - -def map_trace_event_to_state_patch( - event: TraceEvent, - started_steps: int, - completed_steps: int, -) -> _StatePatch | None: - """Map a trace event to a ``Task.update_state`` patch, or ``None`` to skip. - - Phase 2.1 surfaces only pipe-step boundaries (``PipeStartEvent`` / - ``PipeEndSuccessEvent`` / ``PipeEndErrorEvent``). The other trace event - kinds (edges, batch fan-out, controller outputs, execution metadata, - usage reports) are intentionally suppressed — they fire too frequently - to be useful as Mistral state updates and are already captured by - Pipelex's own reporting / graph infrastructure. - - Mirrors the ``isinstance``-chain pattern used in - ``pipelex.tracing.graphspec_assembler`` for the same union of subclasses. - """ - if isinstance(event, PipeStartEvent): - return _state_patch_for_pipe_start(event=event, started_steps=started_steps) - if isinstance(event, PipeEndSuccessEvent): - return _state_patch_for_pipe_end_success(event=event, completed_steps=completed_steps) - if isinstance(event, PipeEndErrorEvent): - return _state_patch_for_pipe_end_error(event=event) - if isinstance( - event, - ( - EdgeEvent, - ControllerOutputEvent, - BatchItemEvent, - BatchAggregateEvent, - ParallelCombineEvent, - ExecutionDataEvent, - UsageReportEvent, - ), - ): - return None - log.warning(f"Streaming forwarder received unknown trace event type: {type(event).__name__}") - return None - - -async def forward_events_to_task( - event_queue: asyncio.Queue[Any], - update_state: Callable[[dict[str, Any]], Awaitable[None]], -) -> None: - """Drain the event queue, translating each trace event into a state update. - - Runs concurrently with ``run_pipe_via_bridge`` and terminates when the - sentinel is observed. Maintains running counters for ``started_steps`` and - ``completed_steps`` so each emitted patch carries the cumulative count - after the event itself (1-indexed: the first PIPE_START reports - ``started_steps=1``). - - The caller is responsible for posting ``SHUTDOWN_SENTINEL`` and awaiting - this coroutine before letting the parent ``Task`` async-context exit, so - the final per-step ``update_state`` calls are flushed before - ``Task.__aexit__`` closes the task. - """ - started_steps = 0 - completed_steps = 0 - while True: - item = await event_queue.get() - if isinstance(item, _ShutdownSentinel): - return - if not isinstance(item, TraceEvent): - log.warning(f"Streaming forwarder received unexpected queue item type: {type(item).__name__}") - continue - - next_started = started_steps + 1 if isinstance(item, PipeStartEvent) else started_steps - next_completed = completed_steps + 1 if isinstance(item, PipeEndSuccessEvent) else completed_steps - patch = map_trace_event_to_state_patch( - event=item, - started_steps=next_started, - completed_steps=next_completed, - ) - if patch is None: - continue - started_steps = next_started - completed_steps = next_completed - await update_state(patch.payload) - - -def get_drain_timeout_seconds() -> float: - """Expose the forwarder drain timeout for streaming.py to use in wait_for.""" - return _FORWARDER_DRAIN_TIMEOUT_SECONDS - - -def get_queue_max_size() -> int: - """Expose the queue max size for streaming.py to use when constructing the queue.""" - return _QUEUE_MAX_SIZE diff --git a/pyproject.toml b/pyproject.toml index 375192dea..563aafb95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,6 @@ google-genai = [ huggingface = ["huggingface_hub>=0.23,<1.0.0"] linkup = ["linkup-sdk>=0.12.0"] mistralai = ["mistralai>=2.4.4"] -mistralai-workflows = ["mistralai-workflows>=3.3.0"] dynamodb = ["boto3>=1.34.131"] s3 = ["boto3>=1.34.131", "aioboto3>=13.4.0"] temporal = ["temporalio==1.24.0", "aiohttp>=3.9.0"] @@ -151,18 +150,6 @@ module = [ "pypdfium2.raw", ] -[[tool.mypy.overrides]] -# Mistral Workflows uses PEP 695 type-parameter syntax that mypy rejects under -# python_version=3.11 even though our runtime supports 3.10+. Skip following -# imports into this third-party package so its source files don't leak into -# our type-check. -follow_imports = "skip" -ignore_errors = true -module = [ - "mistralai.workflows.*", - "mistralai.workflows", -] - [tool.pyright] pythonVersion = "3.11" include = ["pipelex", "tests"] diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py b/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py deleted file mode 100644 index 085a8a240..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/conftest.py +++ /dev/null @@ -1,35 +0,0 @@ -from collections.abc import Generator -from pathlib import Path - -import pytest - -from pipelex.hub import get_func_registry, get_library_manager, set_current_library -from tests.integration.pipelex.plugins.mistralai_workflows.test_data.bridge_funcs import mistralai_workflows_bridge_echo - -TEST_DATA_DIR = Path(__file__).parent / "test_data" - - -@pytest.fixture(scope="class") -def bridge_test_library() -> Generator[str, None, None]: - """Open a class-scoped library populated with the bridge test pipe. - - The pipe ``mistralai_workflows_bridge_test.bridge_func_pipe`` is registered - in the active library, and the matching Python function is registered in - the FuncRegistry. Both are torn down on exit. - """ - func_registry = get_func_registry() - func_registry.register_function(mistralai_workflows_bridge_echo) - - library_manager = get_library_manager() - library_id, _ = library_manager.open_library() - set_current_library(library_id=library_id) - library_manager.load_libraries( - library_id=library_id, - library_dirs=[TEST_DATA_DIR], - ) - try: - yield library_id - finally: - library_manager.teardown(library_id=library_id) - if func_registry.has_function("mistralai_workflows_bridge_echo"): - func_registry.unregister_function_by_name("mistralai_workflows_bridge_echo") diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py deleted file mode 100644 index 4f4d3a8bd..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_direct.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Layer-2 integration test: ``pipelex_run_pipe`` activity end-to-end. - -Spins an in-process Temporal test environment plus a Mistral test worker, -and runs a workflow that invokes ``pipelex_run_pipe`` against a real loaded -Pipelex pipe. Skipped when ``mistralai-workflows`` is not installed. -""" - -from typing import Any - -import pytest -import pytest_asyncio - -mistralai_workflows = pytest.importorskip("mistralai.workflows") - -from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 -from mistralai.workflows.testing import create_test_worker # noqa: E402 # pyright: ignore[reportUnknownVariableType] -from temporalio.common import SearchAttributeKey # noqa: E402 -from temporalio.testing import WorkflowEnvironment # noqa: E402 - -# Pipelex imports must be wrapped in ``imports_passed_through`` because the -# workflow sandbox would otherwise reject our pipelex imports while validating -# the workflow class. Activities themselves run outside the sandbox so the -# wrapped imports are only needed where the workflow body references them. -with mistralai_workflows.workflow.unsafe.imports_passed_through(): - from pipelex.plugins.mistralai_workflows.activities import pipelex_run_pipe - from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - ) - from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" -TEST_TASK_QUEUE = "pipelex-mistralai-workflows-test" - - -@mistralai_workflows.workflow.define( - name="pipelex-bridge-test-workflow", - enforce_determinism=False, # bypass workflow sandbox for the integration test -) -class PipelexBridgeTestWorkflow: - @mistralai_workflows.workflow.entrypoint - async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: - payload = PipelexPipeRunInput.model_validate(payload_dict) - output: PipelexPipeRunOutput = await pipelex_run_pipe(payload) - return output - - -@pytest.fixture(scope="module", autouse=True) -def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] - """Pin Mistral's global task_queue config to our test queue. - - Mistral's ``@activity`` wrapper dispatches via - ``temporalio.workflow.execute_activity(..., task_queue=config.get_effective_task_queue())``, - which reads the global ``mistralai_config.temporal.task_queue`` (default - ``"default"``). If we don't override it, activities are scheduled on - ``"default"`` while the worker polls ``TEST_TASK_QUEUE`` — the activity - never gets picked up and the workflow hangs. - """ - original = mistralai_config.temporal.task_queue - mistralai_config.temporal.task_queue = TEST_TASK_QUEUE - try: - yield - finally: - mistralai_config.temporal.task_queue = original - - -@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] -async def workflow_env(): - # Mistral's workflow.define wraps the run method with code that upserts an - # ``OtelTraceId`` search attribute on every workflow run. The dev server - # rejects the workflow activation if the attribute isn't pre-registered on - # the namespace, so we declare it here. - env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] - search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], - ) - try: - yield env - finally: - await env.shutdown() - - -@pytest.mark.asyncio(loop_scope="class") -class TestPipelexRunPipeActivity: - async def test_workflow_invokes_pipe_via_bridge_in_direct_mode( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - payload = PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "via mistralai workflow"}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - - async with create_test_worker( - workflow_env, - workflows=[PipelexBridgeTestWorkflow], - activities=[pipelex_run_pipe], - task_queue=TEST_TASK_QUEUE, - ): - result_dict = await workflow_env.client.execute_workflow( - PipelexBridgeTestWorkflow.run, - {"payload_dict": payload.model_dump(mode="json")}, - id="pipelex-bridge-test-workflow-direct", - task_queue=TEST_TASK_QUEUE, - ) - - result = PipelexPipeRunOutput.model_validate(result_dict) - assert result.is_completed is True - assert result.workflow_id is None - assert result.main_stuff_name is not None - assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "via mistralai workflow" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py deleted file mode 100644 index 40b13a243..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_offloaded.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Layer-2 integration test: ``pipelex_run_pipe_offloaded`` activity end-to-end. - -Verifies that the ``OffloadableField``-based variant correctly wraps and -unwraps Pipelex payloads through a Mistral Workflows activity. Skipped when -``mistralai-workflows`` is not installed. - -This test does NOT exercise Mistral's actual blob-storage offloading path — -that requires worker-level interceptor configuration with real S3/GCS/Azure -storage. It exercises the wrapping/unwrapping shape (the part Pipelex owns) -so users can confidently configure the offloading interceptor on their own -workers without surprises at the model boundary. -""" - -from typing import Any - -import pytest -import pytest_asyncio - -mistralai_workflows = pytest.importorskip("mistralai.workflows") - -from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 -from mistralai.workflows.core.encoding.fields_offloader import OffloadableField # noqa: E402 -from mistralai.workflows.testing import create_test_worker # noqa: E402 # pyright: ignore[reportUnknownVariableType] -from temporalio.common import SearchAttributeKey # noqa: E402 -from temporalio.testing import WorkflowEnvironment # noqa: E402 - -with mistralai_workflows.workflow.unsafe.imports_passed_through(): - from pipelex.plugins.mistralai_workflows.activities import ( - PipelexPipeRunInputOffloaded, - PipelexPipeRunOutputOffloaded, - pipelex_run_pipe_offloaded, - ) - from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - ) - from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" -TEST_TASK_QUEUE = "pipelex-mistralai-workflows-offloaded-test" - -# Larger than Mistral's default offloading threshold (typically a few KiB), -# small enough that the in-process Temporal test server still accepts it -# inline. Keeps the test self-contained while exercising a non-trivial -# payload through the OffloadableField wrapper. -LARGE_INPUT_SIZE_BYTES = 200 * 1024 - - -@mistralai_workflows.workflow.define( - name="pipelex-bridge-offloaded-test-workflow", - enforce_determinism=False, -) -class PipelexBridgeOffloadedTestWorkflow: - @mistralai_workflows.workflow.entrypoint - async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: - inner = PipelexPipeRunInput.model_validate(payload_dict) - wrapped = PipelexPipeRunInputOffloaded(payload=OffloadableField(value=inner)) - result: PipelexPipeRunOutputOffloaded = await pipelex_run_pipe_offloaded(wrapped) - unwrapped: PipelexPipeRunOutput = result.payload.get_value() - return unwrapped - - -@pytest.fixture(scope="module", autouse=True) -def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] - """Pin Mistral's global task_queue config to our test queue (see test_activities_direct.py).""" - original = mistralai_config.temporal.task_queue - mistralai_config.temporal.task_queue = TEST_TASK_QUEUE - try: - yield - finally: - mistralai_config.temporal.task_queue = original - - -@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] -async def workflow_env(): - env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] - search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], - ) - try: - yield env - finally: - await env.shutdown() - - -@pytest.mark.asyncio(loop_scope="class") -class TestPipelexRunPipeOffloadedActivity: - async def test_offloaded_activity_round_trips_large_payload( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - large_text = "x" * LARGE_INPUT_SIZE_BYTES - payload = PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": large_text}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - - async with create_test_worker( - workflow_env, - workflows=[PipelexBridgeOffloadedTestWorkflow], - activities=[pipelex_run_pipe_offloaded], - task_queue=TEST_TASK_QUEUE, - ): - result_dict = await workflow_env.client.execute_workflow( - PipelexBridgeOffloadedTestWorkflow.run, - {"payload_dict": payload.model_dump(mode="json")}, - id="pipelex-bridge-offloaded-test-workflow", - task_queue=TEST_TASK_QUEUE, - ) - - result = PipelexPipeRunOutput.model_validate(result_dict) - assert result.is_completed is True - assert result.workflow_id is None - assert result.main_stuff_name is not None - echoed_text = result.output_dict["root"][result.main_stuff_name]["content"]["text"] - assert echoed_text == large_text diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py deleted file mode 100644 index 10c03c2cc..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_activities_streaming.py +++ /dev/null @@ -1,259 +0,0 @@ -"""Layer-2 integration test: ``pipelex_run_pipe_streaming`` activity end-to-end. - -Spins an in-process Temporal test environment plus a Mistral test worker -configured with the ``EventInterceptor``, runs a workflow that invokes -``pipelex_run_pipe_streaming``, and asserts that the lifecycle events -(``CustomTaskStarted`` → ``CustomTaskInProgress`` → ``CustomTaskCompleted``) -were published with the expected ``custom_task_type`` and payload shape. - -Skipped when ``mistralai-workflows`` is not installed. -""" - -from typing import Any, cast - -import pytest -import pytest_asyncio - -mistralai_workflows = pytest.importorskip("mistralai.workflows") - -from mistralai.workflows.core._events.event_context import EventContext # noqa: E402, PLC2701 -from mistralai.workflows.core.config.config import config as mistralai_config # noqa: E402 -from mistralai.workflows.protocol.v1.events import ( # noqa: E402 - CustomTaskCompleted, - CustomTaskInProgress, - CustomTaskStarted, - WorkflowEvent, -) -from mistralai.workflows.testing import ( # noqa: E402 - create_capturing_mock_events_client, - create_test_worker_with_events, # pyright: ignore[reportUnknownVariableType] -) -from temporalio.common import SearchAttributeKey # noqa: E402 -from temporalio.testing import WorkflowEnvironment # noqa: E402 - -with mistralai_workflows.workflow.unsafe.imports_passed_through(): - from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - PipelexPipeRunOutput, - ) - from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - from pipelex.plugins.mistralai_workflows.streaming import ( - PIPELEX_PIPE_RUN_TASK_TYPE, - pipelex_run_pipe_streaming, - ) - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" -PIPE_REF_SEQUENCE = "mistralai_workflows_bridge_test.bridge_sequence_pipe" -TEST_TASK_QUEUE = "pipelex-mistralai-workflows-streaming-test" - - -@mistralai_workflows.workflow.define( - name="pipelex-bridge-streaming-test-workflow", - enforce_determinism=False, -) -class PipelexBridgeStreamingTestWorkflow: - @mistralai_workflows.workflow.entrypoint - async def run(self, payload_dict: dict[str, Any]) -> PipelexPipeRunOutput: - payload = PipelexPipeRunInput.model_validate(payload_dict) - output: PipelexPipeRunOutput = await pipelex_run_pipe_streaming(payload) - return output - - -@pytest.fixture(scope="module", autouse=True) -def override_mistralai_task_queue(): # pyright: ignore[reportUnusedFunction] - """Pin Mistral's global task_queue to our test queue (see test_activities_direct.py).""" - original = mistralai_config.temporal.task_queue - mistralai_config.temporal.task_queue = TEST_TASK_QUEUE - try: - yield - finally: - mistralai_config.temporal.task_queue = original - - -@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] -async def workflow_env(): - env = await WorkflowEnvironment.start_local( # pyright: ignore[reportUnknownMemberType] - search_attributes=[SearchAttributeKey.for_keyword("OtelTraceId")], - ) - try: - yield env - finally: - await env.shutdown() - - -@pytest.mark.asyncio(loop_scope="class") -class TestPipelexRunPipeStreamingActivity: - async def test_workflow_emits_custom_task_lifecycle_events( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - captured_events: list[WorkflowEvent] = [] - mock_events_client = create_capturing_mock_events_client(captured_events) - - payload = PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "via streaming activity"}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - - async with ( - EventContext(events_client=mock_events_client), - create_test_worker_with_events( - workflow_env, - workflows=[PipelexBridgeStreamingTestWorkflow], - activities=[pipelex_run_pipe_streaming], - task_queue=TEST_TASK_QUEUE, - ), - ): - result_dict = await workflow_env.client.execute_workflow( - PipelexBridgeStreamingTestWorkflow.run, - {"payload_dict": payload.model_dump(mode="json")}, - id="pipelex-bridge-streaming-test-workflow", - task_queue=TEST_TASK_QUEUE, - ) - - result = PipelexPipeRunOutput.model_validate(result_dict) - assert result.is_completed is True - assert result.main_stuff_name is not None - assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "via streaming activity" - - custom_task_events = [ - event - for event in captured_events - if isinstance(event, (CustomTaskStarted, CustomTaskInProgress, CustomTaskCompleted)) - and event.attributes.custom_task_type == PIPELEX_PIPE_RUN_TASK_TYPE - ] - - # Expect exactly one Started, at least one InProgress (the "completed" state update), - # and one Completed event for the pipe-run task. - started_events = [event for event in custom_task_events if isinstance(event, CustomTaskStarted)] - in_progress_events = [event for event in custom_task_events if isinstance(event, CustomTaskInProgress)] - completed_events = [event for event in custom_task_events if isinstance(event, CustomTaskCompleted)] - - assert len(started_events) == 1, f"expected 1 CustomTaskStarted, got {len(started_events)}" - assert len(in_progress_events) >= 1, f"expected >=1 CustomTaskInProgress, got {len(in_progress_events)}" - assert len(completed_events) == 1, f"expected 1 CustomTaskCompleted, got {len(completed_events)}" - - started_payload = started_events[0].attributes.payload.value - assert started_payload["pipe_code"] == PIPE_REF - assert started_payload["phase"] == "started" - assert started_payload["execution_mode"] == PipelexExecutionMode.DIRECT - - completed_payload = completed_events[0].attributes.payload.value - assert completed_payload["phase"] == "completed" - assert completed_payload["pipeline_run_id"] == result.pipeline_run_id - assert completed_payload["main_stuff_name"] == result.main_stuff_name - - async def test_multistep_pipe_emits_per_step_events( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - """A two-step PipeSequence produces one CustomTaskInProgress per pipe boundary.""" - captured_events: list[WorkflowEvent] = [] - mock_events_client = create_capturing_mock_events_client(captured_events) - - payload = PipelexPipeRunInput( - pipe_code=PIPE_REF_SEQUENCE, - inputs={"input_text": "step events"}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - - async with ( - EventContext(events_client=mock_events_client), - create_test_worker_with_events( - workflow_env, - workflows=[PipelexBridgeStreamingTestWorkflow], - activities=[pipelex_run_pipe_streaming], - task_queue=TEST_TASK_QUEUE, - ), - ): - result_dict = await workflow_env.client.execute_workflow( - PipelexBridgeStreamingTestWorkflow.run, - {"payload_dict": payload.model_dump(mode="json")}, - id="pipelex-bridge-streaming-multistep-test-workflow", - task_queue=TEST_TASK_QUEUE, - ) - - result = PipelexPipeRunOutput.model_validate(result_dict) - assert result.is_completed is True - assert result.main_stuff_name is not None - # Both steps must have run in declaration order: upper("step events") wrapped with [STEP2:...] - assert result.output_dict["root"][result.main_stuff_name]["content"]["text"] == "[STEP2:STEP EVENTS]" - - custom_task_events = [ - event - for event in captured_events - if isinstance(event, (CustomTaskStarted, CustomTaskInProgress, CustomTaskCompleted)) - and event.attributes.custom_task_type == PIPELEX_PIPE_RUN_TASK_TYPE - ] - started_events = [event for event in custom_task_events if isinstance(event, CustomTaskStarted)] - in_progress_events = [event for event in custom_task_events if isinstance(event, CustomTaskInProgress)] - completed_events = [event for event in custom_task_events if isinstance(event, CustomTaskCompleted)] - - assert len(started_events) == 1, f"expected 1 CustomTaskStarted, got {len(started_events)}" - assert len(completed_events) == 1, f"expected 1 CustomTaskCompleted, got {len(completed_events)}" - - # CustomTaskInProgress carries a JSONPatchPayload — value is a list of JSON Patch operations - # (one per field that *changed* between previous and new state). Flatten each event's patches - # into a {field: value} dict. - # - # Important: a field only appears in the patch when its value actually changed. If two - # consecutive update_state calls write the same value to a field (e.g. last_event_kind back - # to "pipe_start" without a "pipe_end_success" in between), that field is absent from the - # second patch. We therefore key on /started_steps (strictly monotonic on every pipe_start) - # and /completed_steps (strictly monotonic on every pipe_end_success). - patches_per_event = [_patches_to_changes(event) for event in in_progress_events] - - pipe_start_changes = [changes for changes in patches_per_event if "started_steps" in changes] - pipe_end_success_changes = [changes for changes in patches_per_event if "completed_steps" in changes] - assert len(pipe_start_changes) >= 3, f"expected >=3 pipe_start in_progress events, got {len(pipe_start_changes)}" - assert len(pipe_end_success_changes) >= 3, f"expected >=3 pipe_end_success in_progress events, got {len(pipe_end_success_changes)}" - - # Order: outer PipeSequence first, then step_one, then step_two. /current_step_pipe_code - # changes on every pipe_start (each pipe has a distinct code) so it always appears in the patch. - step_codes_in_order = [changes["current_step_pipe_code"] for changes in pipe_start_changes] - assert step_codes_in_order[0].endswith("bridge_sequence_pipe") - assert step_codes_in_order[1].endswith("bridge_seq_step_one") - assert step_codes_in_order[2].endswith("bridge_seq_step_two") - - # started_steps counter is monotonic 1, 2, 3 across the first three pipe_start events. - started_steps_seq = [changes["started_steps"] for changes in pipe_start_changes[:3]] - assert started_steps_seq == [1, 2, 3] - - # completed_steps reaches at least 3 by the end. - max_completed = max(changes["completed_steps"] for changes in pipe_end_success_changes) - assert max_completed >= 3 - - # Phase 2.0 fields still surfaced on the final completed event (a JSONPayload — full state snapshot). - completed_payload = completed_events[0].attributes.payload.value - assert completed_payload["phase"] == "completed" - assert completed_payload["pipeline_run_id"] == result.pipeline_run_id - assert completed_payload["main_stuff_name"] == result.main_stuff_name - - -def _patches_to_changes(event: CustomTaskInProgress) -> dict[str, Any]: - """Flatten a CustomTaskInProgress JSON Patch list into a {field: value} dict. - - Each ``update_state`` call produces a single ``CustomTaskInProgress`` with - a list of root-level "add"/"replace" patches (paths look like ``/field``). - Returns a dict of just the fields that changed in this event. - """ - changes: dict[str, Any] = {} - payload_value: Any = event.attributes.payload.value - if not isinstance(payload_value, list): - return changes - raw_patches = cast("list[Any]", payload_value) - for raw_patch in raw_patches: - if isinstance(raw_patch, dict): - patch_dict = cast("dict[str, Any]", raw_patch) - else: - patch_dict = cast("dict[str, Any]", raw_patch.model_dump()) - op = patch_dict.get("op") - path = patch_dict.get("path", "") - if op in {"add", "replace"} and isinstance(path, str) and path.startswith("/"): - field = path[1:] - if field: - changes[field] = patch_dict.get("value") - return changes diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py deleted file mode 100644 index 7c69b0dce..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_direct.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Layer-1 integration tests for the mistralai_workflows bridge in DIRECT mode. - -These tests do NOT depend on the optional ``mistralai-workflows`` package — they -exercise only the framework-agnostic core (``run_pipe_via_bridge`` with a real -loaded pipe). The activity wrapper is covered separately in -``test_activities_direct.py``, which DOES require the optional dep. -""" - -from typing import Any - -import pytest - -from pipelex.hub import get_library_manager -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, run_pipe_via_bridge -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_func_pipe" - - -@pytest.mark.asyncio(loop_scope="class") -class TestBridgeDirect: - async def test_direct_mode_with_globally_loaded_library( - self, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - """Bridge runs a pipe found in the active library when no crate is provided.""" - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "hello world"}, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - - assert result.is_completed is True - assert result.workflow_id is None - assert result.main_stuff_name is not None - main_stuff_dump = result.output_dict["root"][result.main_stuff_name] - assert main_stuff_dump["content"]["text"] == "hello world" - - async def test_direct_mode_with_library_crate_dump( - self, - bridge_test_library: str, - ) -> None: - """Bridge round-trips through ``library_crate_dump`` end-to-end. - - Captures a LibraryCrate from the loaded library, pipes it through the - bridge as a JSON-safe dict, and verifies the pipe still resolves and - runs against the per-call scoped library that the bridge opens. - """ - crate = get_library_manager().get_crate(library_id=bridge_test_library) - assert crate is not None - crate_dump: dict[str, Any] = crate.model_dump(mode="json") - - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "via crate"}, - library_crate_dump=crate_dump, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - - assert result.is_completed is True - assert result.main_stuff_name is not None - main_stuff_dump = result.output_dict["root"][result.main_stuff_name] - assert main_stuff_dump["content"]["text"] == "via crate" - - async def test_direct_mode_uses_caller_pipeline_run_id( - self, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - """Caller-supplied ``pipeline_run_id`` propagates to the PipeJob.""" - caller_run_id = "caller-supplied-run-id" - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "trace me"}, - pipeline_run_id=caller_run_id, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - - assert result.is_completed is True - assert result.pipeline_run_id == caller_run_id - - async def test_direct_mode_dynamic_concept_round_trips_via_library_crate_dump( - self, - bridge_test_library: str, - ) -> None: - """A concept with an inline structure round-trips through ``library_crate_dump``. - - ``EchoEnvelope`` is defined inline in the bridge_test bundle. The bridge - dehydrates the library to a JSON-safe crate dump, opens a per-call - scoped library on the receiving side, and re-hydrates the concept so - ``PipeCompose`` can construct a ``StructuredContent`` matching the - dynamic shape. - """ - envelope_pipe_ref = "mistralai_workflows_bridge_test.bridge_envelope_pipe" - crate = get_library_manager().get_crate(library_id=bridge_test_library) - assert crate is not None - crate_dump: dict[str, Any] = crate.model_dump(mode="json") - - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=envelope_pipe_ref, - inputs={"input_text": "wrapped"}, - library_crate_dump=crate_dump, - execution_mode=PipelexExecutionMode.DIRECT, - ) - ) - - assert result.is_completed is True - assert result.main_stuff_name is not None - main_stuff = result.output_dict["root"][result.main_stuff_name] - content = main_stuff["content"] - assert content["text"] == "wrapped" - assert content["origin"] == "mistralai_workflows_bridge" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py deleted file mode 100644 index bc75c9bfc..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_blocking.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Layer-3 integration test: ``run_pipe_via_bridge`` end-to-end in TEMPORAL_BLOCKING mode. - -The bridge dispatches a Pipelex ``WfPipeRun`` workflow through the same -``make_temporal_pipe_run`` helper that the activity wrapper uses, and waits -for completion. This validates the full bridge → Pipelex Temporal wiring. - -The Mistral ``@activity`` wrapping over ``run_pipe_via_bridge`` is a -single-line decoration already validated end-to-end in DIRECT mode by -``test_activities_direct.py``; the same wrapping flows through this code path -unchanged. - -Skipped when ``temporalio`` (or ``mistralai-workflows``) is not installed. -""" - -from collections.abc import AsyncGenerator, Generator - -import pytest -import pytest_asyncio - -pytest.importorskip("temporalio") -pytest.importorskip("mistralai.workflows") - -from temporalio.testing import WorkflowEnvironment - -from pipelex.cogt.content_generation.generated_content_factory import GeneratedContentFactory -from pipelex.config import get_config -from pipelex.hub import get_pipelex_hub, get_storage_provider -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.temporal.tasks import Tasks -from pipelex.temporal.temporal_data_converter import data_converter -from pipelex.temporal.temporal_hub import get_task_manager, temporal_hub -from pipelex.temporal.temporal_manager import TemporalManager, get_temporal_manager -from pipelex.temporal.temporal_task_manager import TemporalTaskManager -from pipelex.temporal.tprl_content_generation.content_generator_child_factory import ContentGeneratorChildFactory -from pipelex.temporal.tprl_pipe.temporal_pipe_router import make_temporal_pipe_router - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_compose_pipe" -TEST_TASK_QUEUE = "pipelex-bridge-temporal-blocking-test" - - -@pytest.fixture(scope="module", autouse=True) -def _enable_pipelex_temporal_for_bridge() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] - """Enable Pipelex Temporal and pin the worker task_queue to our test queue. - - The bridge calls ``make_temporal_pipe_run()`` with no arguments, which - reads the task_queue from ``get_config().temporal.worker_config.task_queue``. - """ - config = get_config() - original_is_enabled = config.temporal.is_enabled - original_task_queue = config.temporal.worker_config.task_queue - config.temporal.is_enabled = True - config.temporal.worker_config.task_queue = TEST_TASK_QUEUE - try: - yield - finally: - config.temporal.is_enabled = original_is_enabled - config.temporal.worker_config.task_queue = original_task_queue - - -@pytest.fixture(scope="module", autouse=True) -def _boot_pipelex_temporal_layer(_enable_pipelex_temporal_for_bridge: None) -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] - """Set up the Pipelex Temporal task manager + temporal-aware routers. - - Mirrors the production boot path: registers WfPipeRun / WfPipeRouter and - swaps the pipe_router and content_generator on the hub for their - Temporal-aware variants. Without this, dispatching ``WfPipeRun`` would - fail because the worker would have no workflows to register. - """ - manager = TemporalTaskManager() - temporal_hub.set_task_manager(manager) - manager.complement_catalog( - extra_catalog=Tasks.TASK_PACKS, - extra_workflows=[], - extra_activities=[], - ) - manager.setup() - - pipelex_hub = get_pipelex_hub() - original_pipe_router = pipelex_hub.get_required_pipe_router() - original_content_generator = pipelex_hub.get_required_content_generator() - - pipelex_hub.set_pipe_router(make_temporal_pipe_router()) - generated_content_factory = GeneratedContentFactory(storage_provider=get_storage_provider()) - pipelex_hub.set_content_generator( - ContentGeneratorChildFactory.make_content_generator_child( - generated_content_factory=generated_content_factory, - ) - ) - - TemporalManager.setup(session_id="bridge-temporal-blocking-test") - - try: - yield - finally: - TemporalManager.teardown() - pipelex_hub.set_pipe_router(original_pipe_router) - pipelex_hub.set_content_generator(original_content_generator) - manager.teardown() - temporal_hub.reset() - - -@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] -async def workflow_env() -> AsyncGenerator[WorkflowEnvironment, None]: - """Local Temporal env wired with Pipelex's data converter. - - Pre-connects ``TemporalManager`` to ``env.client`` so that - ``make_temporal_pipe_run()`` (called by the bridge with default - ``should_auto_connect_temporal=True``) reuses the same client instead of - auto-connecting to a non-existent production server. - """ - env = await WorkflowEnvironment.start_local(data_converter=data_converter) # pyright: ignore[reportUnknownMemberType] - try: - await get_temporal_manager().connect_temporal(temporal_client=env.client) - yield env - finally: - await env.shutdown() - - -@pytest.mark.temporal -@pytest.mark.asyncio(loop_scope="class") -class TestBridgeTemporalBlocking: - async def test_temporal_blocking_mode_end_to_end( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - """Bridge dispatches WfPipeRun on the test Temporal server and blocks until completion.""" - async with get_task_manager().make_worker( - temporal_client=workflow_env.client, - task_queue=TEST_TASK_QUEUE, - is_not_sandboxed=True, - ): - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "hello via temporal blocking"}, - execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, - ) - ) - - assert result.is_completed is True - assert result.workflow_id is not None - assert result.main_stuff_name is not None - main_stuff_dump = result.output_dict["root"][result.main_stuff_name] - assert main_stuff_dump["content"]["text"] == "hello via temporal blocking" diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py deleted file mode 100644 index 9398bca24..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_bridge_temporal_fire_and_forget.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Layer-3 integration test: ``run_pipe_via_bridge`` in TEMPORAL_FIRE_AND_FORGET mode. - -Validates that the bridge dispatches a Pipelex ``WfPipeRun`` workflow on the -test Temporal server and returns immediately without waiting for completion, -and that the workflow eventually completes asynchronously. - -Skipped when ``temporalio`` (or ``mistralai-workflows``) is not installed. -""" - -from collections.abc import AsyncGenerator, Generator - -import pytest -import pytest_asyncio - -pytest.importorskip("temporalio") -pytest.importorskip("mistralai.workflows") - -from temporalio.client import WorkflowExecutionStatus -from temporalio.testing import WorkflowEnvironment - -from pipelex.cogt.content_generation.generated_content_factory import GeneratedContentFactory -from pipelex.config import get_config -from pipelex.hub import get_pipelex_hub, get_storage_provider -from pipelex.pipe_run.delivery_assignment import DeliveryAssignment -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode -from pipelex.temporal.tasks import Tasks -from pipelex.temporal.temporal_data_converter import data_converter -from pipelex.temporal.temporal_hub import get_task_manager, temporal_hub -from pipelex.temporal.temporal_manager import TemporalManager, get_temporal_manager -from pipelex.temporal.temporal_task_manager import TemporalTaskManager -from pipelex.temporal.tprl_content_generation.content_generator_child_factory import ContentGeneratorChildFactory -from pipelex.temporal.tprl_pipe.temporal_pipe_router import make_temporal_pipe_router - -PIPE_REF = "mistralai_workflows_bridge_test.bridge_compose_pipe" -TEST_TASK_QUEUE = "pipelex-bridge-temporal-fire-and-forget-test" - - -@pytest.fixture(scope="module", autouse=True) -def _enable_pipelex_temporal_for_bridge() -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] - config = get_config() - original_is_enabled = config.temporal.is_enabled - original_task_queue = config.temporal.worker_config.task_queue - config.temporal.is_enabled = True - config.temporal.worker_config.task_queue = TEST_TASK_QUEUE - try: - yield - finally: - config.temporal.is_enabled = original_is_enabled - config.temporal.worker_config.task_queue = original_task_queue - - -@pytest.fixture(scope="module", autouse=True) -def _boot_pipelex_temporal_layer(_enable_pipelex_temporal_for_bridge: None) -> Generator[None, None, None]: # pyright: ignore[reportUnusedFunction] - """Mirrors the production boot path for Pipelex's Temporal layer.""" - manager = TemporalTaskManager() - temporal_hub.set_task_manager(manager) - manager.complement_catalog( - extra_catalog=Tasks.TASK_PACKS, - extra_workflows=[], - extra_activities=[], - ) - manager.setup() - - pipelex_hub = get_pipelex_hub() - original_pipe_router = pipelex_hub.get_required_pipe_router() - original_content_generator = pipelex_hub.get_required_content_generator() - - pipelex_hub.set_pipe_router(make_temporal_pipe_router()) - generated_content_factory = GeneratedContentFactory(storage_provider=get_storage_provider()) - pipelex_hub.set_content_generator( - ContentGeneratorChildFactory.make_content_generator_child( - generated_content_factory=generated_content_factory, - ) - ) - - TemporalManager.setup(session_id="bridge-temporal-faf-test") - - try: - yield - finally: - TemporalManager.teardown() - pipelex_hub.set_pipe_router(original_pipe_router) - pipelex_hub.set_content_generator(original_content_generator) - manager.teardown() - temporal_hub.reset() - - -@pytest_asyncio.fixture(scope="module") # pyright: ignore[reportUntypedFunctionDecorator, reportUnknownMemberType] -async def workflow_env() -> AsyncGenerator[WorkflowEnvironment, None]: - env = await WorkflowEnvironment.start_local(data_converter=data_converter) # pyright: ignore[reportUnknownMemberType] - try: - await get_temporal_manager().connect_temporal(temporal_client=env.client) - yield env - finally: - await env.shutdown() - - -@pytest.mark.temporal -@pytest.mark.asyncio(loop_scope="class") -class TestBridgeTemporalFireAndForget: - async def test_fire_and_forget_returns_immediately_and_workflow_completes( - self, - workflow_env: WorkflowEnvironment, - bridge_test_library: str, # noqa: ARG002 - ) -> None: - """Bridge starts WfPipeRun without waiting; the workflow completes asynchronously. - - Asserts in two phases: - - 1. The bridge returns with ``is_completed=False`` and a non-None - ``workflow_id`` — proving the dispatch did not block. - 2. The Pipelex Temporal workflow eventually completes with - ``COMPLETED`` status when the worker is given time to run. - """ - delivery_assignment_dump = DeliveryAssignment().model_dump(mode="json") - - async with get_task_manager().make_worker( - temporal_client=workflow_env.client, - task_queue=TEST_TASK_QUEUE, - is_not_sandboxed=True, - ): - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code=PIPE_REF, - inputs={"input_text": "hello via temporal fire and forget"}, - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - delivery_assignment_dump=delivery_assignment_dump, - ) - ) - - # Phase 1 — dispatch returned immediately without waiting. - assert result.is_completed is False - assert result.workflow_id is not None - assert result.output_dict == {} - assert result.main_stuff_name is None - - # Phase 2 — the Pipelex workflow eventually completes on the worker. - handle = workflow_env.client.get_workflow_handle(workflow_id=result.workflow_id) - await handle.result() - description = await handle.describe() - assert description.status == WorkflowExecutionStatus.COMPLETED diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py deleted file mode 100644 index 78caeb56b..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_funcs.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Test functions registered for the mistralai_workflows bridge integration tests.""" - -from pipelex.core.memory.working_memory import WorkingMemory -from pipelex.core.stuffs.text_content import TextContent - - -def mistralai_workflows_bridge_echo(working_memory: WorkingMemory) -> TextContent: - """Echo the ``input_text`` stuff back as a TextContent output. - - Used by tests/integration/pipelex/plugins/mistralai_workflows to validate - end-to-end pipe execution through the bridge without invoking inference. - """ - input_text = working_memory.get_stuff_as_str("input_text") - return TextContent(text=input_text) diff --git a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds b/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds deleted file mode 100644 index 9cc1f048c..000000000 --- a/tests/integration/pipelex/plugins/mistralai_workflows/test_data/bridge_test.mthds +++ /dev/null @@ -1,56 +0,0 @@ -domain = "mistralai_workflows_bridge_test" -description = "Test pipes for the mistralai_workflows plugin bridge" - -[concept.EchoEnvelope] -description = "Custom dynamic concept used to exercise library_crate_dump round-trip for inline-structured concepts." - -[concept.EchoEnvelope.structure] -text = { type = "text", required = true, description = "The echoed text" } -origin = { type = "text", required = true, description = "Origin marker for the echo" } - -[pipe.bridge_func_pipe] -type = "PipeFunc" -description = "Echoes the input text back as output (DIRECT mode only — PipeFunc is not Temporal-compatible)" -output = "Text" -function_name = "mistralai_workflows_bridge_echo" - -[pipe.bridge_compose_pipe] -type = "PipeCompose" -description = "Echoes the input_text via a Jinja2 template (Temporal-compatible)" -inputs = { input_text = "Text" } -output = "Text" -template = "{{ input_text.text }}" - -[pipe.bridge_envelope_pipe] -type = "PipeCompose" -description = "Composes the input_text into a structured EchoEnvelope, exercising dynamic-concept round-trip via library_crate_dump" -inputs = { input_text = "Text" } -output = "EchoEnvelope" - -[pipe.bridge_envelope_pipe.construct] -text = { from = "input_text.text" } -origin = "mistralai_workflows_bridge" - -[pipe.bridge_seq_step_one] -type = "PipeCompose" -description = "First step of bridge_sequence_pipe — uppercases the input text" -inputs = { input_text = "Text" } -output = "Text" -template = "{{ input_text.text | upper }}" - -[pipe.bridge_seq_step_two] -type = "PipeCompose" -description = "Second step of bridge_sequence_pipe — wraps the result with markers" -inputs = { uppercased = "Text" } -output = "Text" -template = "[STEP2:{{ uppercased.text }}]" - -[pipe.bridge_sequence_pipe] -type = "PipeSequence" -description = "Two-step sequence used by Phase 2.1 streaming tests to assert per-step events" -inputs = { input_text = "Text" } -output = "Text" -steps = [ - { pipe = "bridge_seq_step_one", result = "uppercased" }, - { pipe = "bridge_seq_step_two", result = "final_text" }, -] diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py deleted file mode 100644 index efd163fcd..000000000 --- a/tests/unit/pipelex/plugins/mistralai_workflows/test_dispatch.py +++ /dev/null @@ -1,121 +0,0 @@ -import pytest -from pytest_mock import MockerFixture - -from pipelex.core.memory.working_memory_factory import WorkingMemoryFactory -from pipelex.core.pipes.pipe_output import PipeOutput -from pipelex.pipe_run.pipe_job import PipeJob -from pipelex.pipe_run.pipe_run import PipeRun -from pipelex.pipe_run.pipe_run_params_factory import PipeRunParamsFactory -from pipelex.pipeline.job_metadata import JobMetadata -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, run_pipe_via_bridge -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - - -def _make_fake_pipe_job(mocker: MockerFixture, pipe_code: str, pipeline_run_id: str) -> PipeJob: - """Build a PipeJob without triggering Pydantic's PipeAbstract validation. - - Tests at the dispatch layer don't care about the concrete pipe — only - that the bridge routes the right pipe_job to the right PipeRun. Using - ``model_construct`` lets us pass a MagicMock as ``pipe`` without - constructing a full PipeAbstract subclass. - """ - fake_pipe = mocker.MagicMock() - fake_pipe.code = pipe_code - fake_pipe.domain_code = "fake_domain" - return PipeJob.model_construct( - pipe=fake_pipe, - working_memory=WorkingMemoryFactory.make_empty(), - pipe_run_params=PipeRunParamsFactory.make_run_params(), - job_metadata=JobMetadata(user_id="anonymous", pipeline_run_id=pipeline_run_id), - library_crate=None, - ) - - -@pytest.mark.asyncio -class TestDispatch: - async def test_direct_mode_calls_pipe_run_with_pipe_job(self, mocker: MockerFixture) -> None: - fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") - mocker.patch( - "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", - return_value=fake_job, - ) - - fake_output = PipeOutput( - working_memory=WorkingMemoryFactory.make_empty(), - pipeline_run_id="injected-run-id", - ) - mock_run = mocker.patch.object(PipeRun, "run", new_callable=mocker.AsyncMock, return_value=fake_output) - - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code="fake_pipe", - execution_mode=PipelexExecutionMode.DIRECT, - pipeline_run_id="caller-run-id", - ) - ) - - assert mock_run.await_count == 1 - await_args = mock_run.await_args - assert await_args is not None - call_kwargs: dict[str, object] = dict(await_args.kwargs) - assert call_kwargs["delivery_assignment"] is None - assert call_kwargs["pipe_job"] is fake_job - - assert result.is_completed is True - assert result.pipeline_run_id == "injected-run-id" - assert result.workflow_id is None - assert result.graph_spec_dump is None - - async def test_temporal_blocking_dispatches_to_temporal_pipe_run(self, mocker: MockerFixture) -> None: - fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") - mocker.patch( - "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", - return_value=fake_job, - ) - - fake_output = PipeOutput( - working_memory=WorkingMemoryFactory.make_empty(), - pipeline_run_id="temporal-run-id", - ) - fake_temporal_run = mocker.AsyncMock(return_value=fake_output) - fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") - fake_factory.return_value.run = fake_temporal_run - - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code="fake_pipe", - execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, - ) - ) - - fake_factory.assert_called_once() - assert fake_temporal_run.await_count == 1 - assert result.is_completed is True - assert result.workflow_id == "temporal-run-id" - - async def test_temporal_fire_and_forget_returns_workflow_id_without_completion(self, mocker: MockerFixture) -> None: - fake_job = _make_fake_pipe_job(mocker=mocker, pipe_code="fake_pipe", pipeline_run_id="caller-run-id") - mocker.patch( - "pipelex.plugins.mistralai_workflows.bridge.build_pipe_job_from_input", - return_value=fake_job, - ) - - fake_handle = mocker.MagicMock() - fake_start = mocker.AsyncMock(return_value=("wf-id-42", fake_handle)) - fake_factory = mocker.patch("pipelex.temporal.tprl_pipe.temporal_pipe_run.make_temporal_pipe_run") - fake_factory.return_value.start = fake_start - - result = await run_pipe_via_bridge( - PipelexPipeRunInput( - pipe_code="fake_pipe", - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - delivery_assignment_dump={"webhooks": [], "storage": None}, - pipeline_run_id="caller-run-id", - ) - ) - - fake_start.assert_awaited_once() - assert result.is_completed is False - assert result.workflow_id == "wf-id-42" - assert result.pipeline_run_id == "caller-run-id" - assert result.output_dict == {} diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py deleted file mode 100644 index 930e3cf89..000000000 --- a/tests/unit/pipelex/plugins/mistralai_workflows/test_execution_mode.py +++ /dev/null @@ -1,18 +0,0 @@ -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - - -class TestPipelexExecutionMode: - def test_string_values_are_stable(self): - assert PipelexExecutionMode.DIRECT == "direct" - assert PipelexExecutionMode.TEMPORAL_BLOCKING == "temporal_blocking" - assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET == "temporal_fire_and_forget" - - def test_requires_pipelex_temporal(self): - assert PipelexExecutionMode.DIRECT.requires_pipelex_temporal is False - assert PipelexExecutionMode.TEMPORAL_BLOCKING.requires_pipelex_temporal is True - assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.requires_pipelex_temporal is True - - def test_is_fire_and_forget(self): - assert PipelexExecutionMode.DIRECT.is_fire_and_forget is False - assert PipelexExecutionMode.TEMPORAL_BLOCKING.is_fire_and_forget is False - assert PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET.is_fire_and_forget is True diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py deleted file mode 100644 index 7e68d9faa..000000000 --- a/tests/unit/pipelex/plugins/mistralai_workflows/test_input_models.py +++ /dev/null @@ -1,69 +0,0 @@ -import pytest -from pydantic import ValidationError - -from pipelex.plugins.mistralai_workflows.bridge import PipelexPipeRunInput, PipelexPipeRunOutput -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - - -class TestInputOutputModels: - def test_input_defaults_match_design(self): - payload = PipelexPipeRunInput(pipe_code="some_pipe") - assert payload.pipe_code == "some_pipe" - assert payload.inputs == {} - assert payload.output_name is None - assert payload.pipeline_run_id is None - assert payload.user_id is None - assert payload.library_crate_dump is None - assert payload.execution_mode is PipelexExecutionMode.DIRECT - assert payload.delivery_assignment_dump is None - - def test_input_forbids_extra_fields(self): - with pytest.raises(ValidationError): - PipelexPipeRunInput.model_validate( - { - "pipe_code": "some_pipe", - "unexpected": "field", - } - ) - - def test_input_requires_pipe_code(self): - with pytest.raises(ValidationError): - PipelexPipeRunInput.model_validate({}) - - def test_input_round_trip_via_json(self): - original = PipelexPipeRunInput( - pipe_code="some_pipe", - inputs={"foo": "bar"}, - execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING, - pipeline_run_id="run-123", - user_id="alice", - ) - round_tripped = PipelexPipeRunInput.model_validate(original.model_dump(mode="json")) - assert round_tripped == original - - def test_output_required_fields(self): - with pytest.raises(ValidationError): - PipelexPipeRunOutput.model_validate({"output_dict": {}}) # missing pipeline_run_id and is_completed - - def test_output_forbids_extra_fields(self): - with pytest.raises(ValidationError): - PipelexPipeRunOutput.model_validate( - { - "output_dict": {}, - "pipeline_run_id": "run-1", - "is_completed": True, - "rogue_field": 42, - } - ) - - def test_output_round_trip_via_json(self): - original = PipelexPipeRunOutput( - output_dict={"foo": "bar"}, - main_stuff_name="main", - pipeline_run_id="run-1", - workflow_id=None, - is_completed=True, - graph_spec_dump=None, - ) - round_tripped = PipelexPipeRunOutput.model_validate(original.model_dump(mode="json")) - assert round_tripped == original diff --git a/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py b/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py deleted file mode 100644 index ca59cdd08..000000000 --- a/tests/unit/pipelex/plugins/mistralai_workflows/test_validation.py +++ /dev/null @@ -1,75 +0,0 @@ -import pytest - -from pipelex.libraries.library_crate import LibraryCrate -from pipelex.pipe_run.delivery_assignment import DeliveryAssignment -from pipelex.plugins.mistralai_workflows.bridge import ( - PipelexPipeRunInput, - _decode_delivery_assignment, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] - _decode_library_crate, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] - _validate_input, # noqa: PLC2701 # pyright: ignore[reportPrivateUsage] - run_pipe_via_bridge, -) -from pipelex.plugins.mistralai_workflows.exceptions import PipelexBridgeRuntimeError -from pipelex.plugins.mistralai_workflows.execution_mode import PipelexExecutionMode - - -class TestBridgeValidationAndDecoding: - def test_validate_input_passes_for_direct_without_delivery(self): - payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.DIRECT) - _validate_input(payload) # must not raise - - def test_validate_input_passes_for_temporal_blocking_without_delivery(self): - payload = PipelexPipeRunInput(pipe_code="any", execution_mode=PipelexExecutionMode.TEMPORAL_BLOCKING) - _validate_input(payload) # must not raise - - def test_validate_input_rejects_fire_and_forget_without_delivery(self): - payload = PipelexPipeRunInput( - pipe_code="any", - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - ) - with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): - _validate_input(payload) - - def test_validate_input_accepts_fire_and_forget_with_delivery(self): - payload = PipelexPipeRunInput( - pipe_code="any", - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - delivery_assignment_dump={"webhooks": [], "storage": None}, - ) - _validate_input(payload) # must not raise - - def test_decode_library_crate_returns_none_for_none(self): - assert _decode_library_crate(None) is None - - def test_decode_library_crate_round_trips_empty(self): - empty = LibraryCrate() - decoded = _decode_library_crate(empty.model_dump(mode="json")) - assert decoded is not None - assert decoded.concepts == empty.concepts - assert decoded.pipes == empty.pipes - - def test_decode_delivery_assignment_returns_none_for_none(self): - assert _decode_delivery_assignment(None) is None - - def test_decode_delivery_assignment_round_trips(self): - assignment = DeliveryAssignment.model_validate( - { - "storage": {"key_prefix": "runs/abc"}, - "webhooks": [{"url": "https://example.test/hook"}], - } - ) - decoded = _decode_delivery_assignment(assignment.model_dump(mode="json")) - assert decoded is not None - assert decoded.storage is not None - assert decoded.storage.key_prefix == "runs/abc/" # storage validator appends trailing / - assert len(decoded.webhooks) == 1 - assert decoded.webhooks[0].url == "https://example.test/hook" - - @pytest.mark.asyncio - async def test_run_pipe_via_bridge_rejects_fire_and_forget_without_delivery(self): - payload = PipelexPipeRunInput( - pipe_code="any", - execution_mode=PipelexExecutionMode.TEMPORAL_FIRE_AND_FORGET, - ) - with pytest.raises(PipelexBridgeRuntimeError, match="TEMPORAL_FIRE_AND_FORGET"): - await run_pipe_via_bridge(payload) From 5ccd4c33ba365d1078f6643db29dbb725d0873ba Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 13:05:37 +0200 Subject: [PATCH 15/16] Refactor and streamline Mistral Workflows TODOs, consolidating status updates and clarifying remaining tasks for plugin extraction --- TODOS.md | 610 +++++++++++++------------------------------------------ 1 file changed, 137 insertions(+), 473 deletions(-) diff --git a/TODOS.md b/TODOS.md index e45dd85c0..14e1833ab 100644 --- a/TODOS.md +++ b/TODOS.md @@ -1,457 +1,119 @@ # Mistral Workflows ↔ Pipelex — Plugin Extraction TODOS -> **Session status (2026-05-07).** Streams A, B, and C are **fully done and -> verified**. Both repos are green: -> - `pipelex-mistralai-workflows`: `make agent-check` clean, -> `make agent-test` passes (all 8 tests including 3 layer-2 Mistral -> activity tests + 2 layer-3 Temporal-marked tests + 2 fundamentals). -> - `pipelex` (`_workflows/`): `make agent-check` clean (pyright + mypy -> across 1708 files), `make agent-test` passes, all 4 git-grep invariants -> from A11 satisfied. -> -> **Stream D is the only remaining stream**: coordinated landing & PyPI -> publish (D1), cookbook (D2 deferred), risk-watch (D3), workspace docs -> update (D4). Resume entry point: §Stream D below. -> -> **One non-obvious gotcha discovered & fixed this session**: Mistral's -> `get_effective_task_queue()` returns `worker.deployment_name` (not -> `temporal.task_queue`) whenever `deployment_name` is set and doesn't match -> the configured task queue. A developer `.env` with -> `DEPLOYMENT_NAME=BatMac.local` (or anything else) silently routes -> activities to that deployment name, so the in-process test worker — which -> polls `TEST_TASK_QUEUE` — never picks them up and the workflow hangs -> forever. The fixture `override_mistralai_task_queue` in all 3 layer-2 test -> files now also clears `mistralai_config.worker.deployment_name = None`. -> This is the kind of thing Mistral may relax in a future release; if so, -> the override-to-None can become a no-op but should still be left in for -> safety. +## Status ---- - -## Progress snapshot — what was done across sessions - -### Done in `_workflows/` (Stream A) - -- **A1–A5 (refactor inside pipelex)** — `pipelex/runtime_bridge/` package - fully populated with `__init__.py` (empty), `bridge.py`, `bootstrap.py` - (without `get_pipelex_dependency`), `execution_mode.py`, and `exceptions.py` - (with `PipelexRuntimeBridgeError` base + `MissingPipelexTemporalExtraError` + - `PipelexBridgeRuntimeError`; `MistralWorkflowsNotInstalledError` dropped). - All imports rewritten to `pipelex.runtime_bridge.*`. Library-id prefix - changed to `runtime_bridge_`. Install hint changed to - `pip install 'pipelex[temporal]'`. Module docstrings reframed as - framework-agnostic. -- **A6 — Old plugin dir deleted.** `pipelex/plugins/mistralai_workflows/` no - longer exists. -- **A7 — pyproject.toml updated.** The `mistralai-workflows = [...]` extra - removed from `[project.optional-dependencies]`. The - `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` removed. -- **A8 (layer-1 tests)** — `tests/unit/pipelex/runtime_bridge/` populated - with the four unit tests; `tests/integration/pipelex/runtime_bridge/` - populated with `test_bridge_direct.py` + `conftest.py` + `test_data/` - (`bridge_test.mthds` + `bridge_funcs.py`). The old - `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` directories - are deleted. -- **A9 — Docs removed.** `docs/under-the-hood/mistralai-workflows-plugin.md` - + `mistralai-workflows-recipes.md` deleted; the four `mkdocs.yml` lines - removed. -- **A10 — `[Unreleased]` rewritten.** The three plugin-landing bullets are - out; the migration paragraph is in (Stream A, Changed bullet). - -### Done in `pipelex-mistralai-workflows/` (Stream B + Stream C) - -- **B1 — Starter content stripped.** `hello_world.py`, `hello_world.mthds`, - `tests/test_pipelines/`, `tests/e2e/test_pipelex_mistralai_workflows.py` - all deleted. (Empty `tests/e2e/conftest.py` left in place.) -- **B2 — pyproject.toml fully rewritten** (`v0.1.0`, slim deps, `[temporal]` - extra, mypy override for `mistralai.workflows.*`, `pytest-mock` in dev, - pruned markers, `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }`). - Also added `pythonpath = ["tests"]` under `[tool.pytest]` so the - `from integration.test_data.bridge_funcs import ...` import in - `tests/integration/conftest.py` resolves at collection time - (project rule forbids `tests/__init__.py`). -- **B3 — README rewritten.** -- **B4 — CLAUDE.md rewritten.** -- **B5 — CHANGELOG rewritten.** `[Unreleased]` empty; `[v0.1.0]` populated - with the three landing bullets (rewritten paths) plus the Mistral - `Depends`-ready `pipelex_dependency` bullet, and a Changed bullet noting - the namespace migration. -- **B6 — CI audited.** `tests-check.yml` already calls `make install` → - `uv sync --all-extras`, which pulls in the `[temporal]` extra. No edits - needed. -- **B7 — Makefile audit.** Default decision honored: keep `make validate` - as-is. -- **B8 — `uv.lock` refreshed and committed-state.** `uv lock` then - `uv sync --all-extras` ran successfully; the editable `pipelex` install - works (`pipelex==0.26.4` from `file:///Users/lchoquel/repos/Pipelex/_workflows`). - Smoke-imported all six public symbols (`pipelex_run_pipe`, - `pipelex_run_pipe_offloaded`, `pipelex_run_pipe_streaming`, - plus the three `pipelex.runtime_bridge.*` paths) — all OK. -- **C1 — `activities.py` written** in the new repo. Optional-dep guard - dropped; bare imports of `mistralai.workflows.{activity,...}`. Pipelex - imports rewritten to `pipelex.runtime_bridge.bridge`. -- **C2 — `streaming.py` written.** Same edits; sibling import goes to - `pipelex_mistralai_workflows.streaming_event_forwarder`. -- **C3 — `streaming_event_forwarder.py` copied verbatim** (it had no - Mistral or pipelex.plugins imports already). -- **C4 — `dependency.py` written.** Single `pipelex_dependency()` callable - that returns a `Pipelex` instance, designed to be passed to - `mistralai.workflows.Depends(...)`. Booted on first resolve via - `ensure_pipelex_booted()`. (`§0.5` is now considered locked.) -- **C5 — Layer-2 / 3 integration tests moved.** All five test files live in - `pipelex-mistralai-workflows/tests/integration/` with imports rewritten - (`pipelex.plugins.mistralai_workflows.*` → `pipelex.runtime_bridge.*` + - `pipelex_mistralai_workflows.*`). -- **C6 — Test fixtures moved + conftest merged.** The new - `tests/integration/conftest.py` keeps the scaffold's - `check_pipelex_initialized` / `reset_pipelex_config_fixture` and adds - the `bridge_test_library` class-scoped fixture. Test data - (`bridge_test.mthds` + `bridge_funcs.py`) copied to - `tests/integration/test_data/`. Domain string - `mistralai_workflows_bridge_test` kept verbatim so the same `.mthds` file - works in both repos. Conftest imports use the - `from integration.test_data.bridge_funcs import ...` style backed by - `pythonpath = ["tests"]` (see B2). - -### Verified in this session - -- New repo `make agent-check` → **clean** (ruff format + lint, plxt format + - lint, pyright = 0 errors, mypy = no issues across 5 source files). -- `.env` for the new repo was added by the user to unblock pipelex boot - during tests (Langfuse public key was missing). -- New repo `make agent-test` was kicked off in background **but had not - finished by the time this pause was written** — see "What's blocking - right now" below. - -## What's blocking right now - -Nothing — all in-repo work is done. Stream D's release / publish steps are -manual and intentional gates, not blockers. +Streams A, B, C **complete and verified**. Both repos green: -## What to do next, in order +- `pipelex-mistralai-workflows`: `make agent-check` clean, `make agent-test` + passes (8 tests: 3 layer-2 Mistral activity, 2 layer-3 Temporal, 2 + fundamentals, 1 dry-run-all). +- `pipelex` (`_workflows/`): `make agent-check` clean (pyright + mypy + across 1708 source files), `make agent-test` passes, all 4 §A11 + `git grep` invariants satisfied. -1. **Stream D — coordinated landing & follow-ups.** - - **D1.** Land Stream A's PR on `pipelex` and ship the matching pipelex - release (the one that introduces `pipelex.runtime_bridge` and the - `[Unreleased]` migration paragraph). Same day, push - `pipelex-mistralai-workflows==0.1.0` to PyPI pinning the just-released - `pipelex` minimum. - - **D2 (deferred).** Cookbook entry — defer per §0.6. - - **D3.** Watch the open risks (version coupling, OffloadableField - import drift, CI test parity). - - **D4.** Update root workspace `CLAUDE.md` to add - `pipelex-mistralai-workflows/` to the repo table. -2. **Before publishing v0.1.0**, strip the `[tool.uv.sources]` editable - override from `pipelex-mistralai-workflows/pyproject.toml` so PyPI builds - resolve `pipelex` from PyPI, not the local worktree. Add the override - back at the start of the next dev cycle. +**Stream D** (coordinated landing & PyPI publish) is the only remaining +stream. See §Stream D below. -## Open questions / decisions the next session should NOT re-derive +## Gotcha to remember -- **§0.1, §0.3, §0.4 are locked AND implemented.** `pipelex.runtime_bridge` - exists; new repo is `0.1.0`; editable `[tool.uv.sources]` override is in - place and proven to work via `uv sync`. -- **§0.2 is locked AND implemented.** All split assignments are realized in - code. The split is complete; no hidden Mistral-shaped helper remains in - `pipelex.runtime_bridge`. -- **§0.5 is now locked.** The Mistral component / dependency wrapper is the - single function `pipelex_dependency` in - `pipelex_mistralai_workflows/dependency.py` — boots Pipelex, returns the - singleton. Passed to `Depends(pipelex_dependency)`. No `LibraryCrate` - helper added (deferred per §C4 second bullet). -- **§0.6 (cookbook entry timing).** Still deferred — do not block on it. +Mistral's `get_effective_task_queue()` returns `worker.deployment_name` +(not `temporal.task_queue`) whenever `deployment_name` is set and doesn't +match the configured task queue. Any `DEPLOYMENT_NAME=...` in `.env` +silently routes activities to that deployment name; an in-process test +worker polling `TEST_TASK_QUEUE` then hangs forever. The +`override_mistralai_task_queue` fixture in all 3 layer-2 test files +clears `mistralai_config.worker.deployment_name = None` to make the test +environment deterministic regardless of host env vars. Leave it in place +even if Mistral relaxes the routing rule in a future release. --- -## Original execution plan (unchanged below this line) - -Concrete execution plan for the migration described in -`wip/mistral-workflows-plugin-extract.md`. Read that file plus the -binding design decisions in `wip/mistral-workflows-sub-module.md` §2 and §4 -before starting any task here. - -**Two repos involved** - -- `_workflows/` — git worktree of `pipelex` on branch - `feature/Adapt-mistral-workflows`. Holds the code being extracted. -- `../pipelex-mistralai-workflows/` — already scaffolded from - `pipelex-starter-python` (currently looks like the starter app, needs to be - converted to a library). - -The new repo's package directory is `pipelex_mistralai_workflows/` and the -PyPI name is `pipelex-mistralai-workflows`. The scaffold version sits at -`0.8.0` (inherited from the starter); we will reset to `0.1.0` as the first -real release of this project. - -**Reference docs (consult these before writing Mistral-facing code)** - -- Mistral Workflows skill (this repo): `.claude/skills/workflows/SKILL.md`. - Especially: - - `references/guides/workflows-plugins.mdx` — the plugin contract (most - relevant to §0.5 and Stream C, task C4). - - `references/guides/dependency-injection.mdx` — `Depends(...)` shape - (relevant to C4). - - `references/guides/streaming.mdx` + `references/guides/streaming-consumption.mdx` - — Task API, `update_state`, event subscription (relevant to Stream C, - task C2). +## End state delivered + +### `_workflows/` (pipelex) + +- New `pipelex/runtime_bridge/` package: `bridge.py`, `bootstrap.py` + (`ensure_pipelex_booted` only — `get_pipelex_dependency` removed), + `execution_mode.py`, `exceptions.py` (`PipelexRuntimeBridgeError` base + + `MissingPipelexTemporalExtraError` + `PipelexBridgeRuntimeError`). + `MistralWorkflowsNotInstalledError` deleted entirely. Library-id prefix + is `runtime_bridge_`. Install hint reads `pip install 'pipelex[temporal]'`. +- `pipelex/plugins/mistralai_workflows/` deleted. +- `tests/{unit,integration}/pipelex/runtime_bridge/` populated with the + layer-1 tests + `conftest.py` + `test_data/` (domain string + `mistralai_workflows_bridge_test` and function name + `mistralai_workflows_bridge_echo` kept verbatim across both repos). + `tests/{unit,integration}/pipelex/plugins/mistralai_workflows/` deleted. +- `pyproject.toml`: `mistralai-workflows` extra removed; the + `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` removed. +- `docs/under-the-hood/mistralai-workflows-{plugin,recipes}.md` deleted; + the four matching `mkdocs.yml` lines removed. +- `CHANGELOG.md` `[Unreleased]` rewritten as a single Changed bullet + describing the migration. + +### `pipelex-mistralai-workflows/` + +- Starter content stripped (`hello_world.{py,mthds}`, `tests/test_pipelines/`, + `tests/e2e/test_pipelex_mistralai_workflows.py`). +- `pyproject.toml`: `version = "0.1.0"`, slim deps + (`pipelex>=0.27.0` + `mistralai-workflows>=3.3.0`), `[temporal]` extra + (`pipelex[temporal]>=0.27.0`), pruned markers (`gha_disabled`, + `dry_runnable`, `temporal`), mypy override for `mistralai.workflows.*`, + `pythonpath = ["tests"]` under `[tool.pytest]` so the + `from integration.test_data.bridge_funcs import ...` import resolves at + runtime (project rule forbids `tests/__init__.py`). +- `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }` — + **dev-only override**. Strip before publishing v0.1.0; re-add at the + start of the next dev cycle when the next breaking change to + `pipelex.runtime_bridge` lands. +- `README.md`, `CLAUDE.md`, `CHANGELOG.md` rewritten. +- `pipelex_mistralai_workflows/` package: `activities.py` (with + `pipelex_run_pipe` + `pipelex_run_pipe_offloaded`), `streaming.py` + (`pipelex_run_pipe_streaming` + `PipelexPipeRunStreamingState`), + `streaming_event_forwarder.py` (writer_id + `"mistralai-workflows-streaming"` kept verbatim), `dependency.py` + (single `pipelex_dependency()` callable shaped for + `mistralai.workflows.Depends(...)`). +- `tests/integration/`: 5 layer-2/3 test files + merged `conftest.py` + (scaffold's `check_pipelex_initialized` + `reset_pipelex_config_fixture` + plus `bridge_test_library` class-scoped fixture from pipelex) + copied + `test_data/` (`bridge_test.mthds` + `bridge_funcs.py`). +- CI (`tests-check.yml`) already runs `make install` → + `uv sync --all-extras`, which installs the `[temporal]` extra. No edits + needed. +- `uv.lock` refreshed; `mistralai-workflows==3.4.0` resolved. + +## Decisions locked (do not re-derive) + +- **§0.1.** Framework-agnostic core lives at `pipelex.runtime_bridge.*` + (the earlier `pipelex.embedding` proposal was rejected). +- **§0.2.** Split assignments: `bridge.py`, `execution_mode.py`, + `bootstrap.py::ensure_pipelex_booted`, agnostic exceptions live in + `pipelex.runtime_bridge`. `activities.py`, `streaming.py`, + `streaming_event_forwarder.py`, the Mistral-shaped dependency wrapper + live in `pipelex_mistralai_workflows`. + `MistralWorkflowsPluginError` + `MistralWorkflowsNotInstalledError` + deleted entirely. +- **§0.3.** New repo version is `0.1.0`. +- **§0.4.** New repo pins `pipelex>=0.27.0`; editable `[tool.uv.sources]` + override for local dev (strip before publish, re-add on next dev cycle). +- **§0.5.** Mistral dependency wrapper is `pipelex_dependency()` — boots + Pipelex, returns the singleton, designed for `Depends(pipelex_dependency)`. + No `LibraryCrate` snapshot helper added (deferred, revisit after first + user feedback). +- **§0.6.** Cookbook entry deferred until after PyPI publish. + +## Reference docs (consult before touching Mistral-facing code) + +- `.claude/skills/workflows/SKILL.md` and especially: + - `references/guides/workflows-plugins.mdx` — plugin contract. + - `references/guides/dependency-injection.mdx` — `Depends(...)` shape. + - `references/guides/streaming.mdx` + + `references/guides/streaming-consumption.mdx` — Task API, + `update_state`, event subscription. - `references/guides/handling-large-data.mdx` — `OffloadableField` and - the offloading interceptor (relevant to C1 and the open - `OffloadableField` import-drift risk in §D3). + the offloading interceptor (relevant to D3's import-drift risk). - Mistral docs: - — official plugin authoring guide. - -**Line numbers in this file are hints, not anchors.** When this doc cites -`pyproject.toml` line 88 or `mkdocs.yml` lines 310–311, those numbers -reflect the state at write-time. If unrelated PRs land first, the lines -shift. The **descriptive text** (e.g. "the -`mistralai-workflows = [...]` entry under `[project.optional-dependencies]`") -is the source of truth — grep for it, don't jump to a stale line number. - ---- - -## 0. Pre-decisions (lock these before writing code) - -Defaults below are the recommended path. Override only if there's a concrete -reason; otherwise proceed. - -- [x] **0.1 — Framework-agnostic core lives at `pipelex/runtime_bridge/`** - (decision locked). -- [x] **0.2 — Mistral-specific bits stay in the new repo, agnostic bits move - to `pipelex.runtime_bridge`.** Split implemented as planned. -- [x] **0.3 — Reset `pipelex-mistralai-workflows` to `0.1.0`.** -- [x] **0.4 — Pin `pipelex>=0.27.0` in the new repo, plus an editable - `[tool.uv.sources]` override for local dev.** Implemented. - Reminder: strip the `[tool.uv.sources]` override before publishing - `v0.1.0` so PyPI builds resolve `pipelex` from PyPI, not a relative - path. -- [x] **0.5 — Mistral component / dependency wrapper shape.** Implemented - as a single `pipelex_dependency()` callable in - `pipelex_mistralai_workflows/dependency.py`. Suitable for - `Depends(pipelex_dependency)`. -- [ ] **0.6 — Cookbook entry timing.** Defer - `pipelex-cookbook/examples/c_advanced/mistral-workflows/` until after - `pipelex-mistralai-workflows==0.1.0` is on PyPI (Stream D). - ---- - -## Stream A — Refactor inside `pipelex` (this worktree) - -Goal: end state where `git grep mistralai_workflows` and `git grep -mistralai-workflows` both return zero hits inside `pipelex/`, and the -framework-agnostic core lives at `pipelex.runtime_bridge.*`. - -### A1. Create the new package - -- [x] Create `pipelex/runtime_bridge/` with an empty `__init__.py`. - -### A2. Move `bridge.py` - -- [x] Move + rewrite imports + rename library-id prefix + update install - hint + reframe docstring. - -### A3. Move `execution_mode.py` - -- [x] Move; docstring slightly reframed away from Mistral-specific wording. - -### A4. Move `bootstrap.py` (split — keep agnostic, drop Mistral-shaped) - -- [x] Move; keep `ensure_pipelex_booted`. `get_pipelex_dependency` removed - (lives in the new repo per C4). - -### A5. Split `exceptions.py` - -- [x] Created `pipelex/runtime_bridge/exceptions.py` with the new base - `PipelexRuntimeBridgeError` + `MissingPipelexTemporalExtraError` + - `PipelexBridgeRuntimeError`. `MistralWorkflowsNotInstalledError` - intentionally dropped. - -### A6. Delete the old plugin directory - -- [x] `pipelex/plugins/mistralai_workflows/` removed. - -### A7. Update `pyproject.toml` - -- [x] `mistralai-workflows` extra removed. -- [x] `[[tool.mypy.overrides]]` block for `mistralai.workflows.*` removed. - -### A8. Move/delete tests - -- [x] Layer-1 unit tests moved to `tests/unit/pipelex/runtime_bridge/`. -- [x] Layer-1 integration test moved to - `tests/integration/pipelex/runtime_bridge/test_bridge_direct.py` - with the conftest + test_data. -- [x] Layer-2 / layer-3 integration test files deleted from - `_workflows/` (they live in the new repo per Stream C). -- [x] Old plugin test directories - (`tests/{unit,integration}/pipelex/plugins/mistralai_workflows/`) - deleted entirely. - -### A9. Move docs - -- [x] Both `under-the-hood/mistralai-workflows-*.md` deleted. -- [x] Four `mkdocs.yml` lines removed. -- [ ] **Optional stub.** Default decision: skip — no - `under-the-hood/mistralai-workflows.md` redirect page added. - -### A10. Update `CHANGELOG.md` - -- [x] The three plugin-landing bullets removed from `[Unreleased]`. -- [x] Migration `Changed` bullet added under `[Unreleased]`. - -### A11. Verify - -- [x] `make cleanderived && make rtm && make agent-check` clean. (`make rtm` - regenerates `_generated_model_sets.py` which `cleanderived` deletes — - pyright fails without it.) -- [x] `make agent-test` green. -- [x] `git grep mistralai_workflows pipelex/ tests/ pyproject.toml` → - remaining hits are all in `tests/integration/pipelex/runtime_bridge/` - test data (domain string `mistralai_workflows_bridge_test`, function - name `mistralai_workflows_bridge_echo`). Per A8's - "minimize churn" decision, these were intentionally kept verbatim; - no production-code reference to the old plugin namespace remains. -- [x] `git grep mistralai-workflows pipelex/ tests/ pyproject.toml` → one - hit, a docstring comment in `test_bridge_direct.py` referring to the - *new* package `pipelex-mistralai-workflows`. Acceptable. -- [x] `git grep mistralai-workflows CHANGELOG.md` → exactly the migration - paragraph. -- [x] `git grep "pipelex.runtime_bridge" pipelex/ tests/` finds 8 files in - the new layout. - -### A12. (Out-of-scope reminder) Verify "make agent-check passes without optional dep" - -The outstanding box from `mistral-workflows-sub-module.md` §Outstanding -("`make agent-check` passes with `mistralai-workflows` NOT installed") -becomes trivially true once A6 + A7 are done — `pipelex` no longer imports -`mistralai.workflows` anywhere. No separate verification step needed. - ---- - -## Stream B — Adapt the `pipelex-mistralai-workflows` scaffold - -### B1. Strip starter content - -- [x] All starter files removed. -- [x] Empty `tests/e2e/` directory + conftest left in place (harmless). - -### B2. Rewrite `pyproject.toml` - -- [x] All bullets implemented (see snapshot above). -- [x] Added `pythonpath = ["tests"]` under `[tool.pytest]` after a runtime - `ModuleNotFoundError: No module named 'integration'` was hit during - the first `make agent-test` attempt. Project rule forbids - `tests/__init__.py`, so the conftest uses - `from integration.test_data.bridge_funcs import ...` and pytest's - `pythonpath` adds `tests/` to `sys.path` at collection time. - -### B3. Replace the README - -- [x] Replaced. - -### B4. Replace `CLAUDE.md` - -- [x] Replaced. - -### B5. Rewrite `CHANGELOG.md` - -- [x] Replaced. Carried the three landing bullets into `[v0.1.0]` Added, - added the dependency-helper bullet, and a Changed bullet for the - namespace migration. - -### B6. Audit `.github/workflows/` - -- [x] Reviewed. `tests-check.yml` already runs `make install` → - `uv sync --all-extras`, which installs the `[temporal]` extra. No - edits needed. The other 7 workflows (lint, package, version, - changelog, cla, guard-branches, github-release) are generic and - reference the right repo. - -### B7. Audit `Makefile` - -- [x] Default decision honored: keep `make validate` as-is (it's a no-op - when there are no `.mthds` in `pipelex_mistralai_workflows/`). - -### B8. Refresh `uv.lock` - -- [x] `uv lock` + `uv sync --all-extras` ran cleanly; the editable - `pipelex` install resolves to the worktree path. - `mistralai-workflows` is locked at `==3.4.0` (the floor is - `>=3.3.0`; both 3.3.0 and 3.4.0 confirmed working once the - `deployment_name` fixture override is in place — see C7). - (Lock file is uncommitted in the new repo's working tree until you - commit.) - ---- - -## Stream C — Move plugin code into the new repo - -### C1. Move `activities.py` - -- [x] Moved + guard dropped + imports rewritten. - -### C2. Move `streaming.py` - -- [x] Moved + guard dropped + imports rewritten. - -### C3. Move `streaming_event_forwarder.py` - -- [x] Copied verbatim. -- [x] writer_id `"mistralai-workflows-streaming"` kept verbatim. - -### C4. Add the Mistral component / dependency wrapper - -- [x] `pipelex_mistralai_workflows/dependency.py` written. Single - `pipelex_dependency()` callable shaped for - `mistralai.workflows.Depends(...)`. -- [ ] Optional `LibraryCrate` snapshot helper — deferred. Reference - Mistral plugin (`mistralai.workflows.plugins.mistralai`) does not - mandate it; revisit after first user feedback. - -### C5. Move integration tests (layer-2 / layer-3) - -- [x] All five files moved with imports rewritten: - - `test_activities_direct.py` - - `test_activities_offloaded.py` - - `test_activities_streaming.py` - - `test_bridge_temporal_blocking.py` - - `test_bridge_temporal_fire_and_forget.py` - -### C6. Move test fixtures - -- [x] `tests/integration/conftest.py` merged with the scaffold's existing - fixtures plus the `bridge_test_library` class-scoped fixture. -- [x] `tests/integration/test_data/{bridge_test.mthds,bridge_funcs.py}` - copied across. - -### C7. Verify the new repo - -- [x] `make agent-check` clean. -- [x] `make agent-test` green — all 8 tests pass (3 layer-2 activity - tests, 2 layer-3 Temporal tests, 2 fundamentals, 1 dry-run-all). - **Required test fixture fix**: in all 3 layer-2 test files, the - `override_mistralai_task_queue` fixture also clears - `mistralai_config.worker.deployment_name = None`. Without this, a - developer `.env` with `DEPLOYMENT_NAME=...` (or any non-test value) - causes Mistral's `get_effective_task_queue()` to route activities - to that deployment name instead of `TEST_TASK_QUEUE`, leading to a - silent workflow hang. -- [x] Smoke imports already validated: - - ```python - from pipelex_mistralai_workflows.activities import pipelex_run_pipe, pipelex_run_pipe_offloaded - from pipelex_mistralai_workflows.streaming import pipelex_run_pipe_streaming - from pipelex_mistralai_workflows.dependency import pipelex_dependency - from pipelex.runtime_bridge.bridge import PipelexPipeRunInput, PipelexPipeRunOutput, run_pipe_via_bridge - from pipelex.runtime_bridge.execution_mode import PipelexExecutionMode - from pipelex.runtime_bridge.bootstrap import ensure_pipelex_booted - ``` - -### C8. First release - -- [ ] Tag `v0.1.0` in `pipelex-mistralai-workflows`. -- [ ] Push the tag and create the GitHub release. -- [ ] Publish to PyPI as `pipelex-mistralai-workflows==0.1.0`. -- [ ] Coordinate with the matching pipelex release (Stream D §D1). --- @@ -459,13 +121,21 @@ becomes trivially true once A6 + A7 are done — `pipelex` no longer imports ### D1. Coordinated land -- [ ] Land Stream A's PR on `pipelex` and ship the matching pipelex release - (containing the `pipelex.runtime_bridge` package and the migration - paragraph in CHANGELOG). -- [ ] On the same day, push `pipelex-mistralai-workflows==0.1.0` to PyPI - pinning `pipelex>=0.27.0` to match the freshly-released pipelex. - -### D2. Cookbook entry (deferred from Phase 1.3) +- [ ] Strip the `[tool.uv.sources]` editable override from + `pipelex-mistralai-workflows/pyproject.toml` so PyPI builds resolve + `pipelex` from PyPI. +- [ ] Land Stream A's PR on `pipelex` and ship the matching pipelex + release (the one that introduces `pipelex.runtime_bridge` and the + `[Unreleased]` migration paragraph). +- [ ] Tag `v0.1.0` in `pipelex-mistralai-workflows`, push the tag, create + the GitHub release, publish to PyPI as + `pipelex-mistralai-workflows==0.1.0` pinning `pipelex>=0.27.0` + (or whichever pipelex version actually ships). +- [ ] Re-add the `[tool.uv.sources]` override at the start of the next + dev cycle when the next breaking change to `pipelex.runtime_bridge` + lands. + +### D2. Cookbook entry (deferred) - [ ] In `pipelex-cookbook/`, create `examples/c_advanced/mistral-workflows/`: @@ -478,17 +148,16 @@ becomes trivially true once A6 + A7 are done — `pipelex` no longer imports ### D3. Watch the open risks -- [ ] **Version coupling.** Document the `pipelex.runtime_bridge` public - surface as stable in pipelex docs. A breaking change to that surface - is a breaking change for the plugin pkg. -- [ ] **OffloadableField import drift.** `activities.py` (now in the new - repo) imports `OffloadableField, OffloadableModel` from - `mistralai.workflows.core.encoding.fields_offloader`. If a Mistral - upgrade moves the path, fix in the plugin pkg. -- [ ] **CI test parity.** Layer-2/3 tests now run only in - `pipelex-mistralai-workflows` CI. Make sure both repos' matrices are - green before flipping the switch (i.e. before merging Stream A's PR - to `main` and publishing v0.1.0). +- **Version coupling.** Document the `pipelex.runtime_bridge` public + surface as stable in pipelex docs. A breaking change to that surface is + a breaking change for `pipelex-mistralai-workflows`. +- **OffloadableField import drift.** `pipelex_mistralai_workflows/activities.py` + imports `OffloadableField, OffloadableModel` from + `mistralai.workflows.core.encoding.fields_offloader`. If a Mistral + upgrade moves the path, fix in the plugin pkg. +- **CI test parity.** Layer-2/3 tests now run only in + `pipelex-mistralai-workflows` CI. Make sure both repos' matrices are + green before flipping the switch. ### D4. Workspace docs @@ -500,16 +169,11 @@ becomes trivially true once A6 + A7 are done — `pipelex` no longer imports ## Resume guide -If you're picking this up cold: - -1. Read `wip/mistral-workflows-sub-module.md` §2 and §4 — the binding - design decisions and gotchas. Treat as spec; don't re-derive. -2. Read `wip/mistral-workflows-plugin-extract.md` end-to-end — the - strategy. This file (`TODOS.md`) is the execution layer. -3. §0 pre-decisions are locked except §0.6 (cookbook timing — still - deferred). Do not re-debate. -4. Pick up from §Progress snapshot's "What to do next, in order" — the - pending items are A11 verification + the in-flight new-repo - `make agent-test` outcome + Stream D landing. -5. After every step: `make agent-check && make agent-test` in whichever - repo you touched. +1. Read **§Status** + **§Gotcha to remember** above. That's the whole + in-flight context. +2. The next concrete actions are all in §Stream D. D1 is the gate; + D2/D3/D4 follow. +3. After any further code change: `make agent-check && make agent-test` + in whichever repo you touched. Note: `make cleanderived` deletes + `tests/integration/pipelex/fixtures/_generated_model_sets.py`; run + `make rtm` after `cleanderived` or pyright will fail. From 9338ff233e9277c5c6496bff1301d6f049b76159 Mon Sep 17 00:00:00 2001 From: Louis Choquel Date: Thu, 7 May 2026 13:31:24 +0200 Subject: [PATCH 16/16] TODO cleanup --- TODOS.md | 71 +++++++++++++------------------------------------------- 1 file changed, 16 insertions(+), 55 deletions(-) diff --git a/TODOS.md b/TODOS.md index 14e1833ab..c1e7a4287 100644 --- a/TODOS.md +++ b/TODOS.md @@ -5,14 +5,16 @@ Streams A, B, C **complete and verified**. Both repos green: - `pipelex-mistralai-workflows`: `make agent-check` clean, `make agent-test` - passes (8 tests: 3 layer-2 Mistral activity, 2 layer-3 Temporal, 2 - fundamentals, 1 dry-run-all). + passes (layer-2 Mistral activity, layer-3 Temporal, fundamentals, + dry-run-all). - `pipelex` (`_workflows/`): `make agent-check` clean (pyright + mypy - across 1708 source files), `make agent-test` passes, all 4 §A11 + across the source tree), `make agent-test` passes, all §A11 `git grep` invariants satisfied. -**Stream D** (coordinated landing & PyPI publish) is the only remaining -stream. See §Stream D below. +Release/landing is **not** in scope for now. The dev-only +`[tool.uv.sources]` editable override in +`pipelex-mistralai-workflows/pyproject.toml` stays in place; both repos +are usable side-by-side via that override. ## Gotcha to remember @@ -63,9 +65,7 @@ even if Mistral relaxes the routing rule in a future release. `from integration.test_data.bridge_funcs import ...` import resolves at runtime (project rule forbids `tests/__init__.py`). - `[tool.uv.sources] pipelex = { path = "../_workflows", editable = true }` — - **dev-only override**. Strip before publishing v0.1.0; re-add at the - start of the next dev cycle when the next breaking change to - `pipelex.runtime_bridge` lands. + **dev-only override**. Strip if/when publishing to PyPI. - `README.md`, `CLAUDE.md`, `CHANGELOG.md` rewritten. - `pipelex_mistralai_workflows/` package: `activities.py` (with `pipelex_run_pipe` + `pipelex_run_pipe_offloaded`), `streaming.py` @@ -96,12 +96,12 @@ even if Mistral relaxes the routing rule in a future release. deleted entirely. - **§0.3.** New repo version is `0.1.0`. - **§0.4.** New repo pins `pipelex>=0.27.0`; editable `[tool.uv.sources]` - override for local dev (strip before publish, re-add on next dev cycle). + override for local dev (strip if/when publishing). - **§0.5.** Mistral dependency wrapper is `pipelex_dependency()` — boots Pipelex, returns the singleton, designed for `Depends(pipelex_dependency)`. No `LibraryCrate` snapshot helper added (deferred, revisit after first user feedback). -- **§0.6.** Cookbook entry deferred until after PyPI publish. +- **§0.6.** Cookbook entry deferred. ## Reference docs (consult before touching Mistral-facing code) @@ -117,53 +117,16 @@ even if Mistral relaxes the routing rule in a future release. --- -## Stream D — Coordinated landing & follow-ups - -### D1. Coordinated land - -- [ ] Strip the `[tool.uv.sources]` editable override from - `pipelex-mistralai-workflows/pyproject.toml` so PyPI builds resolve - `pipelex` from PyPI. -- [ ] Land Stream A's PR on `pipelex` and ship the matching pipelex - release (the one that introduces `pipelex.runtime_bridge` and the - `[Unreleased]` migration paragraph). -- [ ] Tag `v0.1.0` in `pipelex-mistralai-workflows`, push the tag, create - the GitHub release, publish to PyPI as - `pipelex-mistralai-workflows==0.1.0` pinning `pipelex>=0.27.0` - (or whichever pipelex version actually ships). -- [ ] Re-add the `[tool.uv.sources]` override at the start of the next - dev cycle when the next breaking change to `pipelex.runtime_bridge` - lands. - -### D2. Cookbook entry (deferred) - -- [ ] In `pipelex-cookbook/`, create - `examples/c_advanced/mistral-workflows/`: - - Tier-1 DIRECT-mode worker script (using `pipelex_run_pipe` from the - new package). - - Tier-2 typed activity exercising `library_crate_dump`. - - README pointing back at the new repo's docs. -- [ ] Update `mistral-workflows-sub-module.md` §Status board to check off - the deferred Phase 1.3 cookbook entry. - -### D3. Watch the open risks - -- **Version coupling.** Document the `pipelex.runtime_bridge` public - surface as stable in pipelex docs. A breaking change to that surface is - a breaking change for `pipelex-mistralai-workflows`. +## Open risks to watch + +- **Version coupling.** A breaking change to the `pipelex.runtime_bridge` + public surface is a breaking change for `pipelex-mistralai-workflows`. - **OffloadableField import drift.** `pipelex_mistralai_workflows/activities.py` imports `OffloadableField, OffloadableModel` from `mistralai.workflows.core.encoding.fields_offloader`. If a Mistral upgrade moves the path, fix in the plugin pkg. - **CI test parity.** Layer-2/3 tests now run only in - `pipelex-mistralai-workflows` CI. Make sure both repos' matrices are - green before flipping the switch. - -### D4. Workspace docs - -- [ ] Update root workspace `CLAUDE.md`'s repository table to include - `pipelex-mistralai-workflows/` (PyPI: `pipelex-mistralai-workflows`, - Python package: `pipelex_mistralai_workflows`). + `pipelex-mistralai-workflows` CI. Keep both repos' matrices green. --- @@ -171,9 +134,7 @@ even if Mistral relaxes the routing rule in a future release. 1. Read **§Status** + **§Gotcha to remember** above. That's the whole in-flight context. -2. The next concrete actions are all in §Stream D. D1 is the gate; - D2/D3/D4 follow. -3. After any further code change: `make agent-check && make agent-test` +2. After any further code change: `make agent-check && make agent-test` in whichever repo you touched. Note: `make cleanderived` deletes `tests/integration/pipelex/fixtures/_generated_model_sets.py`; run `make rtm` after `cleanderived` or pyright will fail.